How to deserialize into IReadOnlyDictionary with C#?

I'm trying to deserialize the JSON

{
  "Type": "Correction",
  "StartTime": "2007-12-19T03:00:00.0000000-08:00",
  "EndTime": "2007-12-23T23:00:00.0000000-08:00",
  "Parameters": [
    {
      "Key": "Something",
      "Value": "1.8"
    },
    {
      "Key": "Something2",
      "Value": "0.10000000000000001"
    },
    {
      "Key": "Something3",
      "Value": "answer3"
    },
  ],
}

Into a DTO including public IReadOnlyDictionary<string, string> Parameters { get; set; } along with many other things.

I'm using the newest Newtonsoft deserializer, with the function

var responseObject = JsonConvert.DeserializeObject<TResponse>(jsonResponse);

But it returns the error

Newtonsoft.Json.JsonSerializationException : Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'System.Collections.Generic.IReadOnlyDictionary`2[System.String,System.String]' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.

Is there any tool I could use to help change the JSON response into a different response such as

"Parameters": 
    {
      "Something": "1.8",
      "Something2": "0.10000000000000001",
      "Something3": "answer3",
    },
  

which works (since the array is removed).

P.S. I've used regex replace, but since the smallest JSON change could cause it to fail, I've giving up on that approach.


Solution 1:

OK this took me a while but I figured it out.

So the short answer is, use the version of NewtonSoft.Json that targets .NET v4.5+ if possible. However if your application is intended to be run on .NET 4.5 and below, you can't use this feature.

The reason you got that error is because your NewtonSoft.Json is targeted to a .NET framework below v4.5. This is because IReadonlyDictionary is introduced in .NET v4.5. This is the blog post back in 2013 introducing this new feature for .NET v4.5 in NewtonSoft 5.0.

In newtonsoft.json nuget package, there are multiple versions of the assembly that target different .NET versions. I used ildasm to peek the assembly metadata.

For packages\Newtonsoft.Json.<version>\lib\net40\Newtonsoft.Json.dll, it has TargetFramework set to v4.0, and its implementation doesn't support deserialize into IReadonlyDictionary:

.custom instance void [mscorlib]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 1A 2E 4E 45 54 46 72 61 6D 65 77 6F 72 6B // ....NETFramework 2C 56 65 72 73 69 6F 6E 3D 76 34 2E 30 01 00 54 // ,Version=v4.0..T 0E 14 46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C // ..FrameworkDispl 61 79 4E 61 6D 65 10 2E 4E 45 54 20 46 72 61 6D // ayName..NET Fram 65 77 6F 72 6B 20 34 ) // ework 4

For packages\Newtonsoft.Json.<version>\lib\net45\Newtonsoft.Json.dll, it has TargetFramework set to v4.5, and its implementation does support deserialize into IReadonlyDictionary

.custom instance void [mscorlib]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 1A 2E 4E 45 54 46 72 61 6D 65 77 6F 72 6B // ....NETFramework 2C 56 65 72 73 69 6F 6E 3D 76 34 2E 35 01 00 54 // ,Version=v4.5..T 0E 14 46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C // ..FrameworkDispl 61 79 4E 61 6D 65 12 2E 4E 45 54 20 46 72 61 6D // ayName..NET Fram 65 77 6F 72 6B 20 34 2E 35 ) // ework 4.5

I even checked a very old version of Newtonsoft.Json (v6.0) that targets .NET 4.5, and it does support the read only dictionary.

Solution 2:

You can write a custom JsonConverter

public class KVListToDictConverter<T1,T2> : Newtonsoft.Json.JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(Dictionary<T1, T2>) == objectType;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
            if (reader.TokenType == JsonToken.StartArray)
                return serializer.Deserialize<List<KeyValuePair<T1, T2>>>(reader).ToDictionary(x => x.Key, x => x.Value);
            else
            {
                var c = serializer.Converters.First();
                serializer.Converters.Clear(); //to avoid infinite recursion
                var dict =  serializer.Deserialize<Dictionary<T1, T2>>(reader);
                serializer.Converters.Add(c);
                return dict;
            }
    }
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

and use in deserialization like

var json = JsonConvert.DeserializeObject<YourObject>(json, new KVListToDictConverter<string,string>());

This would work both for your first json, and also for the one you want to get with regex.