Reputation: 4125
I am trying to deserialize an object with a jagged and multidimensional array property:
public abstract class Foo {}
public class Baz
{
public readonly List<Foo> Foos;
public Baz()
{
Foos = new List<Foo>();
}
}
public class Bar : Foo
{
public readonly double[][,,] Values;
public Bar(double[][,,] values)
{
Values = values;
}
}
Since Baz
has a List<Foo>
and Foo
is an abstract class, I want to preserve types of Foo's in the serialized string, so I have to use TypeNameHandling.All
:
JsonSerializerSettings settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
Formatting = Formatting.Indented
};
However, when I run the following code:
var barValues = new double[][,,] { new double[,,] {{{ 1 }}} };
var baz = new Baz();
baz.Foos.Add(new Bar(barValues));
var json = JsonConvert.SerializeObject(baz, settings);
var baz2 = JsonConvert.DeserializeObject<Baz>(json, settings);
I got an exception:
Type specified in JSON 'System.Double[,][], System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' is not compatible with 'System.Double[,,][], System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e'. Path 'Foos.$values[0].Values.$type', line 9, position 63.'
And if I inspect the serialized string, it looks rather strange:
"Values": {
"$type": "System.Double[,][], System.Private.CoreLib",
...
}
Why JsonConvert
can not deserialize the string in that case ?
Upvotes: 2
Views: 1764
Reputation: 117086
This appears to be a bug with TypeNameHandling.Arrays
and multidimensional arrays of rank > 2.
I can reproduce the problem more easily by serializing a 3d double array using TypeNameHandling.Arrays
:
var root = new double[,,] { { { 1 } } };
var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Arrays };
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
// Try to deserialize to the same type as root
// but get an exception instead:
var root2 = JsonConvert.DeserializeAnonymousType(json, root, settings);
The JSON generated by the code above is:
{
"$type": "System.Double[,], mscorlib",
"$values": [ [ [ 1.0 ] ] ]
}
The presence of the "$type"
property is to be expected, and is documented in TypeNameHandling setting, but as you note it looks wrong: it should have an extra dimension in the array type like so:
"$type": "System.Double[,,], mscorlib",
And in fact I can deserialize the JSON successfully if I manually replace the [,]
with [,,]
like so:
// No exception!
JsonConvert.DeserializeAnonymousType(json.Replace("[,]", "[,,]"), root, settings)
Finally, if I try the same test with a 2d array instead of a 3d array, the test passes. Demo fiddle here.
The cause appears to be a bug in the routine ReflectionUtils.RemoveAssemblyDetails
when called at the following traceback:
Newtonsoft.Json.Utilities.ReflectionUtils.RemoveAssemblyDetails(string) C#
Newtonsoft.Json.Utilities.ReflectionUtils.GetTypeName(System.Type, Newtonsoft.Json.TypeNameAssemblyFormatHandling, Newtonsoft.Json.Serialization.ISerializationBinder) C#
Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.WriteTypeProperty(Newtonsoft.Json.JsonWriter, System.Type) C#
Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.WriteStartArray(Newtonsoft.Json.JsonWriter, object, Newtonsoft.Json.Serialization.JsonArrayContract, Newtonsoft.Json.Serialization.JsonProperty, Newtonsoft.Json.Serialization.JsonContainerContract, Newtonsoft.Json.Serialization.JsonProperty) C#
When called, the input parameter has the value
System.Double[,,], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
But the returned value is
System.Double[,], mscorlib
which is clearly wrong.
An issue could be reported to Newtonsoft here if desired.
Update: a similar issue was opened today: Type of multi-dimensional array is incorrect #1918.
As a workaround, you should limit the scope of properties for which you output type information to situations where a given JSON object might, in practice, be polymorphic. Possibilities include:
You could serialize your object graph with TypeNameHandling.None
but mark your polymorphic collections with JsonPropertyAttribute.ItemTypeNameHandling = TypeNameHandling.Auto
like so:
public class Baz
{
[JsonProperty(ItemTypeNameHandling = TypeNameHandling.Auto)]
public readonly List<Foo> Foos;
public Baz()
{
Foos = new List<Foo>();
}
}
This solution results in less bloated JSON and also minimizes the security risks of using TypeNameHandling
that are described in TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto? and thus is the preferable solution.
You could serialize your object graph with TypeNameHandling.None
and use a custom contract resolver to set JsonArrayContract.ItemTypeNameHandling
to TypeNameHandling.Auto
for collections with potentially polymorphic items, by overriding DefaultContractResolver.CreateArrayContract
.
This would be the solution to use if you cannot add Json.NET attributes to your types.
You could serialize your object graph with TypeNameHandling.Auto
or TypeNameHandling.Objects
.
Either option will avoid the bug and also reduce bloat in your JSON, but will not reduce your security risks.
You could serialize your object graph with JsonSerializerSettings.TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Full
.
This avoids the call to RemoveAssemblyDetails()
but results in even more bloated JSON and does not avoid the possible security risks.
Upvotes: 2