无法保留对数组或只读列表的引用,也不能保留从非默认构造函数创建的列表

时间:2016-12-23 00:00:21

标签: .net serialization json.net

我遇到了下面的问题,这个问题与我遇到的问题大致相同:

JSON.NET cannot handle simple array deserialization?

但是,我的情况略有不同。如果我将该问题的Test类修改为具有相同类型的数组属性,则会出现相同的反序列化错误。

class Test
{
    public Test[] Tests;
}

var settings = new JsonSerializerSettings
{
    PreserveReferencesHandling = PreserveReferencesHandling.All
};

var o = new Test { Tests = new[] { new Test(), new Test() } };
//var o = new Test(); //this works if I leave the Tests array property null
var arr = new[] { o, o };
var ser = JsonConvert.SerializeObject(arr, settings);

arr = ((JArray)JsonConvert.DeserializeObject(ser, settings)).ToObject<Test[]>();

我打赌我错过了Tests属性的重要属性。

3 个答案:

答案 0 :(得分:5)

Json.NET根本没有实现为只读集合和数组保留引用。这在异常消息中明确说明:

  

Newtonsoft.Json.JsonSerializationException:无法保留引用   to array或readonly list,或从非默认创建的列表   构造函数:Question41293407.Test []。

Newtonsoft没有实现这一点的原因是他们的参考跟踪功能旨在能够保留递归自引用。因此,必须在读取其内容之前分配要反序列化的对象,以便在内容反序列化期间可以成功解析嵌套的反向引用。但是,只有在读取其内容后才能为分配只读集合,因为根据定义它只是只读的。

然而,数组的特殊之处在于它们只是“半”只读:它们在分配后无法调整大小,但可以更改单个条目。 (有关此问题的讨论,请参阅Array.IsReadOnly inconsistent depending on interface implementation。)可以利用这一事实为数组创建custom JsonConverter,在读取期间,将JSON加载到中间JToken,分配通过查询令牌的内容获得正确大小的数组,将数组添加到serializer.ReferenceResolver,将内容反序列化为列表,然后最终填充列表中的数组条目:

public class ArrayReferencePreservngConverter : JsonConverter
{
    const string refProperty = "$ref";
    const string idProperty = "$id";
    const string valuesProperty = "$values";

    public override bool CanConvert(Type objectType)
    {
        return objectType.IsArray;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        else if (reader.TokenType == JsonToken.StartArray)
        {
            // No $ref.  Deserialize as a List<T> to avoid infinite recursion and return as an array.
            var elementType = objectType.GetElementType();
            var listType = typeof(List<>).MakeGenericType(elementType);
            var list = (IList)serializer.Deserialize(reader, listType);
            if (list == null)
                return null;
            var array = Array.CreateInstance(elementType, list.Count);
            list.CopyTo(array, 0);
            return array;
        }
        else
        {
            var obj = JObject.Load(reader);
            var refId = (string)obj[refProperty];
            if (refId != null)
            {
                var reference = serializer.ReferenceResolver.ResolveReference(serializer, refId);
                if (reference != null)
                    return reference;
            }
            var values = obj[valuesProperty];
            if (values == null || values.Type == JTokenType.Null)
                return null;
            if (!(values is JArray))
            {
                throw new JsonSerializationException(string.Format("{0} was not an array", values));
            }
            var count = ((JArray)values).Count;

            var elementType = objectType.GetElementType();
            var array = Array.CreateInstance(elementType, count);

            var objId = (string)obj[idProperty];
            if (objId != null)
            {
                // Add the empty array into the reference table BEFORE poppulating it,
                // to handle recursive references.
                serializer.ReferenceResolver.AddReference(serializer, objId, array);
            }

            var listType = typeof(List<>).MakeGenericType(elementType);
            using (var subReader = values.CreateReader())
            {
                var list = (IList)serializer.Deserialize(subReader, listType);
                list.CopyTo(array, 0);
            }

            return array;
        }
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

这种方法的内存效率并不高,因此对于大型集合,最好切换到List<T>

然后使用它:

var settings = new JsonSerializerSettings
{
    Converters = { new ArrayReferencePreservngConverter() },
    PreserveReferencesHandling = PreserveReferencesHandling.All
};
var a2 = JsonConvert.DeserializeObject<Test[]>(jsonString, settings);

请注意,转换器是完全通用的,适用于所有阵列。

示例fiddle显示嵌套递归自引用的反序列化成功。

答案 1 :(得分:1)

我认为这段代码很好但需要精炼

 var elementType = objectType.IsArray ? objectType.GetElementType() : objectType.GetGenericArguments()[0];

objectType.IsGenericType可能属实,因此我们需要使用GetGenericArguments()[0]

答案 2 :(得分:1)

如果有人使用这个转换器,可能会考虑修改这个:

 public override bool CanConvert(Type objectType)
 {
        if (objectType == typeof(byte[]))
            return false; // He would kill a Byte[] and you'll wonder, why the JSON Deserializer will return NULL on Byte[] :-)

        return objectType.IsArray;
 }