It is not possible to save a reference to an array or a read-only list or a list created from a constructor other than the standard

I came across the following question, which is basically identical to the question I have:

JSON.NET can't handle deserializing a simple array?

However, my situation is somewhat different. If I modify the Test class from this question to have an array property of the same type, I get the same deserialization error.

 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[]>(); 

I am sure that the Tests property attribute is missing.

+3
source share
2 answers

Json.NET just didn't implement read-only reference collections and arrays. This is explicitly stated in the exception message:

Newtonsoft.Json.JsonSerializationException: Unable to save reference for array or read-only list or list created from custom constructor: Question41293407.Test [].

The reason Newtonsoft hasn't implemented this is because their link tracking features are designed to preserve recursive links to themselves. Thus, an object that has been deserialized must be selected before reading its contents, so that nested backlinks can be successfully resolved during deserialization of the content. However, a read-only collection can only be assigned after its contents have been read, since by definition it is read-only.

Arrays, however, are peculiar in that they are only β€œsemi” read-only: they cannot be changed after allocation, however, individual records can be changed. (See Array.IsReadOnly, which is not compatible depending on the implementation of the interface , for a discussion of this issue.) You can use this fact to create a custom JsonConverter for arrays that load JSON into an intermediate JToken while reading, allocate an array of the correct size, requesting the contents of the token , adds an array to serializer.ReferenceResolver , deserializes the contents into a list, and then fills the array entries from the list:

 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(); } } 

The memory efficiency of this approach is low, so for large collections it would be better to switch to List<T> .

Then use it like:

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

Please note that the converter is completely general and works for all arrays.

Fiddle example showing successful deserialization of nested recursive self-advertisements.

+4
source

I think this code is good, but needs clarification

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

objectType.IsGenericType may be true, so we need to use GetGenericArguments()[0]

0
source

Source: https://habr.com/ru/post/959150/


All Articles