You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 17:54:49