简体   繁体   English

当 class 具有列表的“快速”访问属性时,Newtonsoft json 存在反序列化错误

[英]Newtonsoft json has a deserialization bug when class has 'quick' access property for list

I have parent class that contains a collection of child POCOs as well as a property that provides quick access to the first member of the collection.我有父 class 包含子 POCO 的集合以及提供对集合的第一个成员的快速访问的属性。 The child POCO types in turn contain another child collection.子 POCO 类型又包含另一个子集合。 When I round-tip the parent using Json.NET, the contents of the first child's collection get duplicated.当我使用 Json.NET 向父级舍入时,第一个子级集合的内容会重复。

What can be done to prevent this?可以做些什么来防止这种情况发生?

Minimal reproducible as follows:最小可重复性如下:

using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;

namespace MyApp // Note: actual namespace depends on the project name.
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var parent = new Parent
            {
                Children = new List<Child>
                {
                    new Child
                    {
                       Children = new List<GrandChild>
                        {
                            new GrandChild { Value = "I am a grand child" },
                        }
                    },
                    new Child(),
                }
            };

            var serializedParent = JsonConvert.SerializeObject(parent);
            var deserializedParent = JsonConvert.DeserializeObject<Parent>(serializedParent);

            // Expected: 1, Actual: 2
            Console.WriteLine(deserializedParent.Children[0].Children.Count);
        }
   }

    public class Parent
    {
         public List<Child> Children { get; set; } = new List<Child>();

         public Child FirstChild => Children.First();
    }

    public class Child
    {
        public List<GrandChild> Children { get; set; } = new List<GrandChild>();
    }

    public class GrandChild
    {
        public string Value { get; set; }
    }
}

Demo fiddle with a failing assertion here .演示在这里摆弄一个失败的断言。

Your problem is that Json.NET will populate a get-only property when it is pre-allocated.您的问题是 Json.NET 在预分配时将填充仅获取属性。 In the case of the FirstChild you need to avoid this, because, in doing so, Json.NET ends up deserializing the contents of FirstChild.Children twice.FirstChild的情况下,您需要避免这种情况,因为这样做,Json.NET 最终会反序列化FirstChild.Children的内容两次。

In detail, the sequence of events during deserialization is as follows:详细来说,反序列化过程中的事件顺序如下:

  1. Since "Children" property happens to come first in the JSON, the Children array is completely deserialized.由于"Children"属性恰好在 JSON 中排在第一位,因此Children数组已完全反序列化。

  2. At this point, FirstChild now returns a Child instance rather than throwing a System.InvalidOperationException: Sequence contains no elements exception.此时, FirstChild现在返回一个Child实例而不是抛出System.InvalidOperationException: Sequence contains no elements异常。

  3. Json.NET now encounters the "FirstChild" property in the JSON, binds it to the Parent.FirstChild c# property. Json.NET 现在在 JSON 中遇到"FirstChild"属性,将其绑定到Parent.FirstChild c# 属性。 Since it is non-null, it populates the contents, which means that FirstChild.Children is populated twice as explained in Json.net deserializing list gives duplicate items .由于它是非空的,它会填充内容,这意味着FirstChild.Children被填充两次,如Json.net 反序列化列表给出重复项中所述

So, what are your workarounds?那么,您的解决方法是什么?

Firstly , if you don't need FirstChild in the JSON, you could simply mark it with [JsonIgnore] which will prevent it from being serialized or deserialized:首先,如果您不需要FirstChild中的 FirstChild,您可以简单地用[JsonIgnore]标记它,这将防止它被序列化或反序列化:

public class Parent
{
    public List<Child> Children { get; set; } = new List<Child>();

    [JsonIgnore]
    public Child FirstChild => Children.First();
}

This will also prevent serialization from failing when Children is empty.这也将防止当Children为空时序列化失败。 Demo fiddle here .演示小提琴在这里

Secondly , if you must have FirstChild present in your JSON, you could attach a JsonConverter to the property that, in ReadJson() , skips the incoming JSON and simply returns the existing value:其次,如果您的 JSON 中必须存在FirstChild ,则可以将JsonConverter附加到在ReadJson()中跳过传入的 JSON 并简单地返回现有值的属性:

public class Parent
{
    public List<Child> Children { get; set; } = new List<Child>();

    [JsonConverter(typeof(SerializeOnlyJsonConverter))]
    public Child FirstChild => Children.First();
    
    public bool ShouldSerializeFirstChild() => Children != null && Children.Count > 0;
}

public class SerializeOnlyJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) => throw new NotImplementedException("This converter should only be applied directly to properties");

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    {
        reader.Skip();
        return existingValue;
    }
    
    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}

You will also need to add a ShouldSerialize method to prevent serializing from failing when Children is null or empty.您还需要添加一个ShouldSerialize方法以防止当Children为 null 或为空时序列化失败。 Or you could fix FirstChild to never throw an exception:或者你可以修复FirstChild永远不会抛出异常:

    public Child FirstChild => Children?.FirstOrDefault();

Demo fiddle #2 here .演示小提琴#2在这里

Thirdly , you could serializing using GetOnlyContractResolver from this answer by Pavlo Lissov to Serialize Property, but Do Not Deserialize Property in Json.Net and apply [GetOnlyJsonProperty] to FirstChild第三,您可以使用Pavlo Lissov GetOnlyContractResolver这个答案中的 GetOnlyContractResolver 序列化来序列化属性,但不要反序列化 Json.Net 中的属性并将[GetOnlyJsonProperty]应用于FirstChild

public class Parent
{
    public List<Child> Children { get; set; } = new List<Child>();

    [GetOnlyJsonProperty]
    public Child FirstChild => Children.First();
    
    public bool ShouldSerializeFirstChild() => Children != null && Children.Count > 0;
}

[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false)]
public class GetOnlyJsonPropertyAttribute : Attribute
{
}

public class GetOnlyContractResolver : DefaultContractResolver
{
    // From this answer https://stackoverflow.com/a/56885301/3744182
    // By https://stackoverflow.com/users/7027460/pavlo-lissov
    // To https://stackoverflow.com/questions/31731320/serialize-property-but-do-not-deserialize-property-in-json-net
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property != null) 
        {
            var attributes = property.AttributeProvider.GetAttributes(typeof(GetOnlyJsonPropertyAttribute), true);
            if (attributes != null && attributes.Count > 0)
                property.ShouldDeserialize = (a) => false;  
        }
        return property;
    }
}

And then serialize as follows:然后序列化如下:

IContractResolver resolver = new GetOnlyContractResolver(); // In production code this should be cached statically to improve performance
var settings = new JsonSerializerSettings { ContractResolver = resolver };

var serializedParent = JsonConvert.SerializeObject(parent, settings);
var deserializedParent = JsonConvert.DeserializeObject<Parent>(serializedParent, settings);

var reserializedParent = JsonConvert.SerializeObject(deserializedParent, settings);

Demo fiddle #3 here .演示小提琴#3在这里

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM