5
votes

Json.NET does not always properly take into account the declared ValueType in a Dictionary<KeyType,ValueType>.

This makes serializing a Dictionary<string,object> pretty unworkable if a value happens to be one for which DefaultContractResolver.CanConvertToString() returns true, unless I'm missing something. Rect is one such type in .NET 4.0. I tried this in Json.NET 4.5r11 and 5.0r2. Consider the following code:

_requestSerializerJson = new JsonSerializer();
// Even setting TypeNameHandling to All doesn't change the deserialized result

Dictionary<string, object> dictionary = new Dictionary<string, object>();
Rect a = new Rect(1, 2, 3, 4);
dictionary.Add("myrect", a);
byte[] bytes;

using (MemoryStream requestStream = new MemoryStream())
using (var streamWriter = new StreamWriter(requestStream))
using (var writer = new JsonTextWriter(streamWriter))
{
    _requestSerializerJson.Serialize(writer, dictionary);
    writer.Flush();
    bytes = requestStream.ToArray();
}
// Serialized to: {"myrect":"1,2,3,4"}

using (MemoryStream stream = new MemoryStream(bytes, 0, bytes.Length))
using (var textReader = new StreamReader(stream))
using (var reader = new JsonTextReader(textReader))
{
    var b = _requestSerializerJson.Deserialize<Dictionary<string, object>>(reader);
}
// b is a Dictionary with a single *string* value "1,2,3,4" instead of a Rect!

Am I thinking about this wrong or missing something? I just switched to Json.NET from XmlSerializer because it is incredibly more performant (especially at construction), and it was all fairly easy to transition to, but running into this issue scares me a bit.

It seems like if Json.NET is going to write something as a string because the object type returns true for CanConvertToString(), it needs to write out a Json attribute indicating that a conversion to string occurred, so that it can be reliably "unconverted" on deserialization...

1
What does Dictionary<string, Rect> do on the deserialize?asawyer
It will work. The problem is when you have a Dictionary of a bunch of different object types for your values.aggieNick02

1 Answers

0
votes

When you're deserializing to a Dictionary<string, object> Json.Net does not have any type information to go on when determining what to instantiate for the dictionary values. The normal solution is either to use a strongly-typed container (e.g. Dictionary<string, Rect>) or to set the TypeNameHandling option to Objects on the serializer. The latter will tell Json.Net to output type metadata with the JSON so that when it is deserializing it knows which type to instantiate.

However, some types like System.Windows.Rect are marked with a [TypeConverter] attribute. When Json.Net finds such a type, it uses the associated TypeConverter to serialize the object to string rather that treating it like a normal object. Unfortunately, when this conversion happens, the original type information is lost, so no metadata gets written out for the value. Which means that unless you are deserializing to a strongly-typed class or container, you will get a string back rather that your original object, and you're back to square one.

You can work around this issue by using a custom ContractResolver that forces Json.Net to serialize the Rect normally instead of using its TypeConverter. Here is the code you would need:

class CustomResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        if (objectType == typeof(System.Windows.Rect))
            return CreateObjectContract(objectType);

        return base.CreateContract(objectType);
    }
}

Here is a round-trip demo using modified code from your question:

JsonSerializer _requestSerializerJson = new JsonSerializer();
_requestSerializerJson.TypeNameHandling = TypeNameHandling.Objects;
_requestSerializerJson.ContractResolver = new CustomResolver();
_requestSerializerJson.Formatting = Formatting.Indented;

Dictionary<string, object> dictionary = new Dictionary<string, object>();
System.Windows.Rect a = new System.Windows.Rect(1, 2, 3, 4);
dictionary.Add("myrect", a);
byte[] bytes;

using (MemoryStream requestStream = new MemoryStream())
using (var streamWriter = new StreamWriter(requestStream))
using (var writer = new JsonTextWriter(streamWriter))
{
    _requestSerializerJson.Serialize(writer, dictionary);
    writer.Flush();
    bytes = requestStream.ToArray();
}

Console.WriteLine(Encoding.UTF8.GetString(bytes));
Console.WriteLine();

Dictionary<string, object> b;

using (MemoryStream stream = new MemoryStream(bytes, 0, bytes.Length))
using (var textReader = new StreamReader(stream))
using (var reader = new JsonTextReader(textReader))
{
    b = _requestSerializerJson.Deserialize<Dictionary<string, object>>(reader);
}

System.Windows.Rect rect = (System.Windows.Rect)b["myrect"];
Console.WriteLine("Left: " + rect.Left);
Console.WriteLine("Top: " + rect.Top);
Console.WriteLine("Width: " + rect.Width);
Console.WriteLine("Height: " + rect.Height);

Output:

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib",
  "myrect": {
    "$type": "System.Windows.Rect, WindowsBase",
    "IsEmpty": false,
    "Location": "1,2",
    "Size": "3,4",
    "X": 1.0,
    "Y": 2.0,
    "Width": 3.0,
    "Height": 4.0,
    "Left": 1.0,
    "Top": 2.0,
    "Right": 4.0,
    "Bottom": 6.0,
    "TopLeft": "1,2",
    "TopRight": "4,2",
    "BottomLeft": "1,6",
    "BottomRight": "4,6"
  }
}

Left: 1
Top: 2
Width: 3
Height: 4