As noted in the question System.Text.Json: How to apply a JsonConverter for a collection with a custom converter for the collection's items?, System.Text.Json does not have an equivalent to Json.NET's JsonPropertyAttribute.ItemConverterType
. Thus you will need to emulate it by creating a JsonConverter
decorator that serializes and deserializes dictionaries using your custom value converter.
以下是一个这样的实现. 请注意,它只适用于string
个键的字典:
public class DictionaryValueConverterDecorator<TItemConverter> : JsonConverterFactory where TItemConverter : JsonConverter, new()
{
readonly TItemConverter itemConverter = new TItemConverter();
public override bool CanConvert(Type typeToConvert) =>
GetStringKeyedDictionaryValueType(typeToConvert) != null && GetConcreteTypeToConvert(typeToConvert) != null;
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (!(GetStringKeyedDictionaryValueType(typeToConvert) is {} valueType)
|| !(GetConcreteTypeToConvert(typeToConvert) is {} concreteTypeToConvert))
throw new ArgumentException($"Invalid type {typeToConvert}");
// Clone the incoming options and insert the item converter at the beginning of the clone.
// Then if converter is actually a JsonConverterFactory (e.g. JsonStringEnumConverter) then the correct JsonConverter<T> will be manufactured or fetched.
var modifiedOptions = new JsonSerializerOptions(options);
modifiedOptions.Converters.Insert(0, itemConverter);
var actualInnerConverter = modifiedOptions.GetConverter(valueType);
return (JsonConverter)Activator.CreateInstance(typeof(DictionaryValueConverterDecoratorInner<,>).MakeGenericType(new [] { concreteTypeToConvert, valueType }), new object [] { actualInnerConverter })!;
}
static Type? GetStringKeyedDictionaryValueType(Type typeToConvert)
{
if (!(typeToConvert.GetDictionaryKeyValueType() is {} types))
return null;
if (types[0] != typeof(string))
return null;
return types[1];
}
static Type? GetConcreteTypeToConvert(Type typeToConvert) =>
typeToConvert.IsInterface switch
{
false when typeToConvert.GetConstructor(Type.EmptyTypes) != null =>
typeToConvert,
true when typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() is var generic && (generic == typeof(IDictionary<,>) || generic == typeof(IReadOnlyDictionary<,>)) =>
typeof(Dictionary<,>).MakeGenericType(typeToConvert.GetGenericArguments()),
_ => null,
};
}
internal class DictionaryValueConverterDecoratorInner<TDictionary, TValue> : JsonConverter<TDictionary> where TDictionary : IDictionary<string, TValue>, new()
{
readonly JsonConverter<TValue> innerConverter;
public DictionaryValueConverterDecoratorInner(JsonConverter<TValue> innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
public override TDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return (TDictionary?)(object?)null;
else if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
var dictionary = new TDictionary();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
else if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
var key = reader.GetString().ThrowOnNull();
reader.ReadAndAssert();
var value = innerConverter.Read(ref reader, typeof(TValue), options);
dictionary.Add(key, value!);
}
return dictionary;
}
public override void Write(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var pair in value)
{
writer.WritePropertyName(pair.Key);
if (value == null && !innerConverter.HandleNull)
writer.WriteNullValue();
else
innerConverter.Write(writer, pair.Value, options);
}
writer.WriteEndObject();
}
}
public static partial class JsonExtensions
{
public static ref Utf8JsonReader ReadAndAssert(ref this Utf8JsonReader reader) { if (!reader.Read()) { throw new JsonException(); } return ref reader; }
public static T ThrowOnNull<T>(this T? value, [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class => value ?? throw new ArgumentNullException(paramName);
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
=> (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();
public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
=> type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());
public static Type []? GetDictionaryKeyValueType(this Type type)
=> type.GetDictionaryKeyValueTypes().SingleOrDefaultIfMultiple();
}
public static class LinqExtensions
{
// Copied from this answer https://stackoverflow.com/a/25319572
// By https://stackoverflow.com/users/3542863/sean-rose
// To https://stackoverflow.com/questions/3185067/singleordefault-throws-an-exception-on-more-than-one-element
public static TSource? SingleOrDefaultIfMultiple<TSource>(this IEnumerable<TSource> source)
{
var elements = source.Take(2).ToArray();
return (elements.Length == 1) ? elements[0] : default(TSource);
}
}
然后,假设SaturationJsonConverter
是大约JsonConverter<Saturation>
,修改你的模型如下:
public partial class Model
{
//[JsonProperty(PropertyName = "substances", ItemConverterType = typeof(SaturationJsonConverter))]
[JsonPropertyName("substances"),
JsonInclude,
JsonConverter(typeof(DictionaryValueConverterDecorator<SaturationJsonConverter>))]
protected ConcurrentDictionary<string, Saturation> Substances { get; private set; } = new();
}
并且您的模型应该按要求序列化.演示小提琴#1here.
注:
[JsonInclude]
是强制非公共属性序列化所必需的.
[JsonPropertyName("substances")]
相当于Json. NET的JsonPropertyAttribute.PropertyName
.
即使启用了JsonObjectCreationHandling.Populate
,也不可能使用带有System.Text.Json的自定义转换器来反序列化只读属性,因为现有值没有传递到Read()
中.作为一种解决办法,我添加了一个私有setter,如下所示.如果您不想添加私有setter,则可以向模型添加一个参数化构造函数,如下所示,将其标记为[JsonConstructor]
:
public partial class Model
{
public Model() { }
[JsonConstructor]
public Model(ConcurrentDictionary<string, Saturation> substances) => this.Substances = substances ?? throw new ArgumentNullException(nameof(substances));
//[JsonProperty(PropertyName = "substances", ItemConverterType = typeof(SaturationJsonConverter))]
[JsonPropertyName("substances"),
JsonInclude,
JsonConverter(typeof(DictionaryValueConverterDecorator<SaturationJsonConverter>))]
protected ConcurrentDictionary<string, Saturation> Substances { get; } = new();
}
here.第一次见面