7

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.

Mikael Dúi Bolinder
  • 2,080
  • 2
  • 19
  • 44
MaxF
  • 93
  • 1
  • 6
  • Can I get away without a custom deserializer? ;) Not quite familiar with those. – MaxF Jul 14 '15 at 17:38
  • Please attach some of the code you're using. The error message you get implies that you're trying to deserialize something of the form "[...]" into an object. – fmt Jul 14 '15 at 17:39
  • 1
    Have you considerer using a `dynamic` type instead of `TResponse`? – Matias Cicero Jul 14 '15 at 17:42
  • Your JSON contains a dictionary serialized as a key/value pair array. `DataContractJsonSerializer` does this by default. You could use the `DictionaryToArrayConverter` from here: http://stackoverflow.com/questions/27332723/usage-specific-serialization-for-complex-type-in-dictionary-with-json-net – dbc Jul 14 '15 at 18:21

4 Answers4

6

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.

KFL
  • 17,162
  • 17
  • 65
  • 89
1

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.

EZI
  • 15,209
  • 2
  • 27
  • 33
  • Hi! Sorry, but the CanConvert throws the exception, since objectType= {Name = "IReadOnlyDictionary`2" FullName = "System.Collections.Generic.IReadOnlyDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"} But the Dictionary is any tips? :) – MaxF Jul 14 '15 at 23:47
1

At the time of writing this you should use a custom converter with System.Text.Json.

Here's a converter for ReadOnlyDictionary<TKey, TValue> and derived types. It assumes that all read-only dictionaries have a constructor that accepts an IDictionary<TKey, TValue> or similar.

It simply deserializes the JSON into a normal Dictionary<TKey, TValue> and then constructs the ReadOnlyDictionary type with that dictionary as an argument.

using System.Collections.ObjectModel;
using System.Reflection;

namespace System.Text.Json.Serialization
{
    public class JsonReadOnlyDictionaryConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))
                return false;

            if ((typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() != typeof(ReadOnlyDictionary<,>)) &&
                !typeof(ReadOnlyDictionary<,>).IsSubclassOfRawGeneric(typeToConvert))
                return false;

            return true;
        }

        public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            var iReadOnlyDictionary = typeToConvert.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>));
            Type keyType = iReadOnlyDictionary.GetGenericArguments()[0];
            Type valueType = iReadOnlyDictionary.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(ReadOnlyDictionaryConverterInner<,>).MakeGenericType(keyType, valueType),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null, args: null, culture: null);

            return converter;
        }

        private class ReadOnlyDictionaryConverterInner<TKey, TValue> : JsonConverter<IReadOnlyDictionary<TKey, TValue>>
            where TKey : notnull
        {
            public override IReadOnlyDictionary<TKey, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                var dictionary = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options: options);

                if (dictionary == null)
                    return null;

                return (IReadOnlyDictionary<TKey, TValue>)Activator.CreateInstance(
                    typeToConvert, BindingFlags.Instance | BindingFlags.Public,
                    binder: null, args: new object[] { dictionary }, culture: null);
            }

            public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary<TKey, TValue> dictionary, JsonSerializerOptions options) =>
                JsonSerializer.Serialize(writer, dictionary, options);
        }
    }
}

You could turn it into a custom JsonConverterAttribute and decorate your class/property with it (which I prefer):

namespace System.Text.Json.Serialization
{
    public class JsonReadOnlyDictionaryAttribute : JsonConverterAttribute
    {
        public JsonReadOnlyDictionaryAttribute() : base(typeof(JsonReadOnlyDictionaryConverter))
        {
        }
    }
}

Or use the JsonSerializerOptions:

var serializeOptions = new JsonSerializerOptions
{
    Converters =
    {
        new JsonReadOnlyDictionaryConverter()
    }
};

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);
Bill Menees
  • 2,124
  • 24
  • 25
Mikael Dúi Bolinder
  • 2,080
  • 2
  • 19
  • 44
  • Note: This depends on the IsSubclassOfRawGeneric extension method. The original version of that method came from https://stackoverflow.com/a/457708/1882616. The extension method (which just adds "this" on the first parameter) is at https://extensionmethod.net/csharp/type/issubclassofrawgeneric. – Bill Menees Oct 09 '22 at 13:30
0

You're going to have to do this in 2 steps, I think. If you deserialize Parameters into an array of objects, then you can use

IReadOnlyDictionary<K,V> parametersDict =
    parametersAoO.ToDictionary(v => v.Key, v => v.Value);

to get a dictionary like that.

fmt
  • 993
  • 9
  • 18