.NET7中System.Text.Json的JsonConverterFactory无法正常工作
在.NET 7控制台应用从Newtonsoft.Json迁移到System.Text.Json时遇到问题:多个DTO包含IReadOnlyCollection<MyObject>类型属性,为MyObject(此处为TermData)编写了JsonConverter处理向后兼容逻辑(比如将原始string转为TermData)。已知System.Text.Json不会自动用元素类型的转换器处理集合元素,而Newtonsoft.Json原生支持此功能。
编写代码后,CollectionJsonConverter.CreateConverter()被调用并返回ReadOnlyCollectionJsonConverter,但ReadOnlyCollectionJsonConverter.CanConvert()从未触发,期望该转换器能被调用并借助TermDataConverter完成元素转换(两个转换器已添加到JsonSerializerOptions.Converters)。
转换器工厂
public class CollectionJsonConverter : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { return !IsExcludedType(typeToConvert) && typeToConvert.IsGenericType && IsAssignableToGenericType(typeToConvert, typeof(IEnumerable<>)); } private static bool IsExcludedType(Type type) { return type.IsPrimitive || type == typeof(string); } private static bool IsAssignableToGenericType(Type givenType, Type genericType) { var interfaceTypes = givenType.GetInterfaces() .Where(x => x.IsGenericType) .Select(x => x.GetGenericTypeDefinition()); return interfaceTypes.Contains(genericType); } public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var elementType = typeToConvert.GetGenericArguments()[0]; var converterType = typeof(ReadOnlyCollectionJsonConverter<>).MakeGenericType(elementType); var instance = Activator.CreateInstance(converterType); if (instance is null) { throw new JsonException(); } return (JsonConverter) instance; } }
集合Json转换器
public sealed class ReadOnlyCollectionJsonConverter<TElement> : JsonConverter<IReadOnlyCollection<TElement>> { public override bool CanConvert(Type typeToConvert) { return typeToConvert.IsGenericType && typeToConvert == typeof(IReadOnlyCollection<>) && typeToConvert.GetGenericArguments()[0] == typeof(TElement); } public override IReadOnlyCollection<TElement> Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartArray) { throw new JsonException(); } var list = new List<TElement>(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndArray) { break; } var elementValue = JsonSerializer.Deserialize<TElement>(ref reader, options); if (elementValue is not null) { list.Add(elementValue); } } return list; } public override void Write( Utf8JsonWriter writer, IReadOnlyCollection<TElement> value, JsonSerializerOptions options) { foreach (var element in value) { JsonSerializer.Serialize(writer, element, options); } } }
TermData Json转换器
public class TermDataConverter : JsonConverter<TermData> { public override TermData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType is JsonTokenType.String) { var str = reader.GetString(); return str is not null ? new TermData {Term = str} : null; } return JsonSerializer.Deserialize<TermData>(ref reader, options); } public override void Write(Utf8JsonWriter writer, TermData value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, options); } }
JsonSerializerOptions配置
_jsonSettings = new JsonSerializerOptions(GlobalJsonSerializerSettings.Services) { Converters = { new CollectionJsonConverter(), new TermDataConverter() } };
DTO定义
public record TermData { [JsonPropertyName("trash_id")] public string TrashId { get; init; } = string.Empty; public string Name { get; init; } = string.Empty; public string Term { get; init; } = string.Empty; } public record PreferredTermData { public int Score { get; init; } public IReadOnlyCollection<TermData> Terms { get; init; } = Array.Empty<TermData>(); } public record ReleaseProfileData { [JsonPropertyName("trash_id")] public string TrashId { get; init; } = string.Empty; public string Name { get; init; } = string.Empty; public bool IncludePreferredWhenRenaming { get; init; } public IReadOnlyCollection<TermData> Required { get; init; } = Array.Empty<TermData>(); public IReadOnlyCollection<TermData> Ignored { get; init; } = Array.Empty<TermData>(); public IReadOnlyCollection<PreferredTermData> Preferred { get; init; } = Array.Empty<PreferredTermData>(); }
1. 移除ReadOnlyCollectionJsonConverter<TElement>.CanConvert方法
你的转换器继承自JsonConverter<IReadOnlyCollection<TElement>>,System.Text.Json已经明确知道它能处理IReadOnlyCollection<TElement>类型,重写CanConvert反而会因为逻辑错误(用开放泛型typeof(IReadOnlyCollection<>)和封闭泛型类型比较,永远不成立)导致转换器不生效。直接删掉该方法即可。
2. 缩小CollectionJsonConverter.CanConvert的匹配范围
当前逻辑会匹配所有实现IEnumerable<>的类型(比如List<T>、HashSet<T>),但你只需要处理IReadOnlyCollection<T>,修改判断逻辑:
public override bool CanConvert(Type typeToConvert) { return !IsExcludedType(typeToConvert) && typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>); }
3. 修复ReadOnlyCollectionJsonConverter<TElement>.Write的格式错误
当前Write方法没有写入数组的StartArray和EndArray标记,会生成非法JSON,修正如下:
public override void Write( Utf8JsonWriter writer, IReadOnlyCollection<TElement> value, JsonSerializerOptions options) { writer.WriteStartArray(); foreach (var element in value) { JsonSerializer.Serialize(writer, element, options); } writer.WriteEndArray(); }
4. 解决TermDataConverter.Read的递归调用问题
在处理非string类型时,直接调用JsonSerializer.Deserialize<TermData>会因为当前转换器在配置中导致无限递归,需要创建临时配置移除当前转换器:
public override TermData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType is JsonTokenType.String) { var str = reader.GetString(); return str is not null ? new TermData { Term = str } : null; } var tempOptions = new JsonSerializerOptions(options); tempOptions.Converters.Remove(this); return JsonSerializer.Deserialize<TermData>(ref reader, tempOptions); }
内容的提问来源于stack exchange,提问作者void.pointer

