首页 > 解决方案 > 使用自定义 Newtonsoft JSON 转换器解析带有重复键的 JSON

问题描述

我有一个无效的 JSON,我需要使用 Newtonsoft 进行解析。问题是 JSON 没有使用正确的数组,而是包含数组中每个条目的重复属性。

我有一些工作代码,但真的不确定这是要走的路还是有更简单的方法?

无效的 JSON:

{
    "Quotes": {
        "Quote": {
            "Text": "Hi"
        },
        "Quote": {
            "Text": "Hello"
        }
    }
}

我试图序列化的对象:

class MyTestObject
{
    [JsonConverter(typeof(NewtonsoftQuoteListConverter))]
    public IEnumerable<Quote> Quotes { get; set; }
}

class Quote
{
    public string Text { get; set; }
}

的读取方法JsonConverter

public override IEnumerable<Quote> ReadJson(JsonReader reader, Type objectType, IEnumerable<Quote> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
    if (reader.TokenType == JsonToken.Null)
    {
        return null;
    }

    var quotes = new List<Quote>();
    while (reader.Read())
    {
        if (reader.Path.Equals("quotes", StringComparison.OrdinalIgnoreCase) && reader.TokenType == JsonToken.EndObject)
        {
            // This is the end of the Quotes block. We've parsed the entire object. Stop reading.
            break;
        }
        
        if (reader.Path.Equals("quotes.quote", StringComparison.OrdinalIgnoreCase) && reader.TokenType == JsonToken.StartObject)
        {
            // This is the start of a new Quote object. Parse it.
            quotes.Add(serializer.Deserialize<Quote>(reader));
        }
    }
    
    return quotes;
}

我只需要读取带有重复键的 JSON,而不需要写入。

标签: c#jsonjson.net

解决方案


我可以看到您的转换器存在一些问题:

  1. MyTestObject因为您对路径进行了硬编码,所以当嵌入某些更高级别的容器中时,您的转换器将无法工作。事实上,它可能会使读者定位不正确。

  2. 您的转换器没有正确跳过过去的评论。

  3. 您的转换器在存在时不会填充传入existingValue,这在反序列化 get-only 集合属性时是必需的。

  4. 您没有考虑当前的命名策略

  5. 当遇到截断的文件时,您的转换器不会抛出异常或以其他方式指示错误。

作为替代方案,您可以利用以下事实:当在 JSON 中多次遇到该属性时,Json.NET 将多次调用该属性的 setter,以在DTO"Quote"中使用仅设置的代理属性来累积属性值像这样:

class NewtonsoftQuoteListConverter : JsonConverter<IEnumerable<Quote>>
{
    class DTO
    {
        public ICollection<Quote> Quotes { get; init; }
        public Quote Quote { set => Quotes.Add(value); }
    }

    public override IEnumerable<Quote> ReadJson(JsonReader reader, Type objectType, IEnumerable<Quote> existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        var dto = new DTO { Quotes = existingValue is ICollection<Quote> l && !l.IsReadOnly ? l : new List<Quote>() }; // Reuse existing value if possible
        serializer.Populate(reader, dto); 
        return dto.Quotes;
    }
    
    public override bool CanWrite => true; // Replace with false if you don't need custom serialization.
    
    public override void WriteJson(JsonWriter writer,  IEnumerable<Quote> value, JsonSerializer serializer)
    {
        // Handle naming strategies.
        var name = ((JsonObjectContract)serializer.ContractResolver.ResolveContract(typeof(DTO))).Properties.Where(p => p.UnderlyingName == nameof(DTO.Quote)).First().PropertyName;
    
        writer.WriteStartObject();
        foreach (var item in value)
        {
            writer.WritePropertyName(name);
            serializer.Serialize(writer, item);
        }
        writer.WriteEndObject();
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

通过使用 DTO,会考虑当前的命名约定。

如果您不需要自定义序列化,请覆盖CanWrite并返回false.

演示小提琴在这里


推荐阅读