2
votes

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 ?

1

1 Answers

2
votes

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.