使用自定义 Newtonsoft JSON 转换器解析具有重复键的 JSON

Parse JSON with duplicate keys using custom Newtonsoft JSON converter

我有一个无效的 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,不需要写入。

您不能将 json 反序列化为具有双属性的 c# 实例,因为 c# 不允许这样做。因此,您需要创建不带双重属性的替代 class 并相应地修复 json 。在此之后你可以毫无问题地反序列化。

您可以用几种不同的方法修复 json,但这个对我来说看起来是最简单的

var jsonOrig=" ... your json";

var json=jsonOrig.Replace("Quotes\":{","Quotes\":[{").Replace("}}}","}}]}");

var quotesRoot = JsonConvert.DeserializeObject<QuotesRoot>(json);

 List<QuoteItem> quotes=quotesRoot.Quotes;

结果

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

classes

public partial class QuotesRoot
    {
        [JsonProperty("Quotes")]
        public List<QuoteItem> Quotes { get; set; }
    }

    public partial class QuoteItem
    {
        [JsonProperty("Quote")]
        public Quote Quote { get; set; }
    }

    public partial class Quote
{
    [JsonProperty("Text")]
    public string Text { get; set; }
}

我发现您的转换器存在一些问题:

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

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

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

  4. 你没有考虑当前的naming strategy

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

作为替代方案,您可以利用 Json.NET 会在 属性 多次遇到 属性 时多次调用 setter在 JSON 中,用 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 和 return false

演示 fiddle here.