0

My goal is to deserialize this JSON into a Dictionary<string, TValue>.

[
  {
    "key": "foo",
    "value": 42
  }
]

My approach is a custom JsonConverter<Dictionary<string, TValue>> with an appropriate JsonConverterFactory.

Deserialization is working fine, but I'm struggling with the serialization part. During serialization, I want this to be serialized like any other dictionary, so the result from serializing the same value as above should be

{
  "foo": 42
}

My idea was to just pass it down to the default JsonDictionaryConverter<TDictionary, TKey, TValue>. This is my Write method:

public override void Write(Utf8JsonWriter writer, Dictionary<string, TValue> value, JsonSerializerOptions options)
{
    var newOptions = new JsonSerializerOptions(options);
    newOptions.Converters.Remove(this);
    JsonSerializer.Serialize(writer, value, newOptions);
}

However I always get a NullReferenceException in JsonDictionaryConverter<TDictionary, TKey, TValue>OnTryWrite(Utf8JsonWriter writer, TDictionary dictionary, JsonSerializerOptions options, ref WriteStack state). The Exception happens here (Line 299), because state.Current.JsonTypeInfo.ElementTypeInfo is null.

state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;

Does anyone have an idea how I can just skip my converter during serialization or have this Exception go away?

wertzui
  • 5,148
  • 3
  • 31
  • 51
  • System.Text.Json has no equivalent to Json.NET's `JsonConverter.CanWrite`. For one workaround, see [How to use default serialization in a custom System.Text.Json JsonConverter?](https://stackoverflow.com/q/65430420/3744182). – dbc Mar 30 '22 at 12:51
  • I'm doing exactly what is described in the linked answer (remove my converter from the options und use the default serialization). However that gives me the mentioned Exception. – wertzui Mar 30 '22 at 12:56
  • Are you using compile-time source generation? Can you share a [mcve], as well as the full `ToString()` output of the exception including the exception type, message, traceback and inner exception(s) if any? – dbc Mar 30 '22 at 13:09
  • As an alternative you might use custom `JsonSerializerOptions` for your model formatter for this specific controller. See [Change the JSON serialization settings of a single ASP.NET Core controller](https://stackoverflow.com/a/52623772/3744182). – dbc Mar 30 '22 at 13:11
  • Your code does seem a little different than the code from [How to use default serialization in a custom System.Text.Json JsonConverter?](https://stackoverflow.com/a/65430421/3744182), You are removing the inner `JsonConverter` from the options list like so: `newOptions.Converters.Remove(this);`. The answer there removes the factory, not the inner converter. Maybe that accounts for the difference? A [mcve] would clear things up. – dbc Mar 30 '22 at 13:47

3 Answers3

1

Following How to use default serialization in a custom System.Text.Json JsonConverter?, you are trying to generate a default serialization by copying the current JsonSerializerOptions and removing the current converter from its converters list. Your problem seems to be here:

newOptions.Converters.Remove(this);

You are using the factory converter pattern, and this line removes the inner, nested JsonDictionaryConverter<TDictionary, TKey, TValue> from newOptions.Converters. However, you need to remove the outer factory from the converters list, not the inner generated converter, as it is the factory that was added into the converters list previously.

The following is a working JsonConverterFactory that deserializes any IDictionary<string, TValue> that might be an array. When re-serializing, it removes itself from the converter list which results in default serialization:

public class JsonDictionaryConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert) => TryGetDictionaryType(typeToConvert, out _, out _, out  _);
    
    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        TryGetDictionaryType(typeToConvert, out var dictionaryType, out var keyType, out var valueType);        
        var converterType = typeof(JsonDictionaryConverter<,,>).MakeGenericType(dictionaryType, keyType, valueType);
        return (JsonConverter)Activator.CreateInstance(converterType, options, this);
    }
    
    static bool TryGetDictionaryType(Type typeToConvert, out Type dictionaryType, out Type keyType, out Type valueType)
    {
        var keyValueTypes = typeToConvert.GetDictionaryKeyValueType();
        if (keyValueTypes != null && keyValueTypes.Length == 2)
        {
            // Todo; maybe put a restriction here on keyType, such as keyType == typeof(string).
            if (typeToConvert.IsInterface)
            {
                var concreteType = typeof(Dictionary<,>).MakeGenericType(keyValueTypes);
                if (typeToConvert.IsAssignableFrom(concreteType))
                {
                    dictionaryType = concreteType;
                    keyType = keyValueTypes[0];
                    valueType = keyValueTypes[1];
                    return true;
                }
            }
            else
            {
                if (typeToConvert.GetConstructor(Type.EmptyTypes) != null)
                {
                    dictionaryType = typeToConvert;
                    keyType = keyValueTypes[0];
                    valueType = keyValueTypes[1];
                    return true;
                }
            }
        }
        dictionaryType = keyType = valueType = null;
        return false;
    }

    class JsonDictionaryConverter<TDictionary, TKey, TValue> : JsonConverter<TDictionary> where TDictionary : IDictionary<TKey, TValue>, new()
    {
        readonly JsonSerializerOptions modifiedOptions;
        
        public JsonDictionaryConverter(JsonSerializerOptions options, JsonDictionaryConverterFactory factory) => this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
    
        public override void Write(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options) =>  JsonSerializer.Serialize(writer, value, modifiedOptions);
            
        public override TDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 
        {
            switch (reader.TokenType)
            {
                case JsonTokenType.StartObject:
                    return (TDictionary)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);
                case JsonTokenType.StartArray:
                    var list = JsonSerializer.Deserialize<List<KeyValuePair<TKey, TValue>>>(ref reader, modifiedOptions);
                    var dictionary = new TDictionary();
                    foreach (var item in list)
                        dictionary.Add(item);
                    return dictionary;
                default:
                    throw new JsonException();
            }
        }
    }
}

public static class JsonExtensions
{
    public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
    {
        var copy = new JsonSerializerOptions(options);
        for (var i = copy.Converters.Count - 1; i >= 0; i--)
            if (copy.Converters[i].GetType() == converterType)
                copy.Converters.RemoveAt(i);
        return copy;
    }

    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
        => (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();

    public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
        => type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());

    public static Type [] GetDictionaryKeyValueType(this Type type)
        => type.GetDictionaryKeyValueTypes().SingleOrDefaultIfMultiple();

    // Copied from this answer https://stackoverflow.com/a/25319572
    // By https://stackoverflow.com/users/3542863/sean-rose
    // To https://stackoverflow.com/questions/3185067/singleordefault-throws-an-exception-on-more-than-one-element
    public static TSource SingleOrDefaultIfMultiple<TSource>(this IEnumerable<TSource> source)
    {
        var elements = source.Take(2).ToArray();
        return (elements.Length == 1) ? elements[0] : default(TSource);
    }
}

And then add to options as follows:

var options = new JsonSerializerOptions
{
    Converters = { new JsonDictionaryConverterFactory() },
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

Notes:

  • I set PropertyNamingPolicy = JsonNamingPolicy.CamelCase because your "key" and "value" property names are camel cased. If you do not want to use camel case globally, inside ReadJson() you may wish to deserialize to a List<KeyValueDTO> (such as the one shown in this answer to System.Text.Json Serialize dictionary as array) with explicitly camel-cased property names, rather than to a List<KeyValuePair<TKey, TValue>>.

  • You may want to add some restrictions in TryGetDictionaryType() on the key type, e.g. to only allow string type keys.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
0

I suggest using helper class and ToDictionary.

var json = @"[
    {
      ""key"": ""foo"",
      ""value"": 42
    }
  ]";

var o = JsonSerializer.Deserialize<List<Z>>(json, new JsonSerializerOptions { IncludeFields = true});
var d = o!.ToDictionary( x => x.key, x => x.value);

Console.WriteLine(JsonSerializer.Serialize(d, new JsonSerializerOptions {WriteIndented = true, IncludeFields = true}));

class Z { public string? key {get;set; } public int? value {get; set;} }

This prints

{
  "foo": 42
}
tymtam
  • 31,798
  • 8
  • 86
  • 126
  • The serializer must deserialize values which are coming in as a POST body. Because of that it should be registered in the global Converters, used by the ASP.Net Core pipeline and automatically selected if the model is a Dictionary or contains a Dictionary. – wertzui Mar 30 '22 at 11:36
  • Use List on your input type and Dict on your output type. I'd say it is correct to use different input and output types if they look different. – tymtam Mar 30 '22 at 11:37
-2

You May Refer I Use JSON convert for convert datatable to JSON format

public static DataTable JsonToDataTable(string Json)
        {
            DataTable dt = new DataTable();
            try
            {
                dt = (DataTable)JsonConvert.DeserializeObject(Json, (typeof(DataTable)));
            }
            catch (Exception ex)
            {
                string chk = ex.Message;
                dt = new DataTable();
            }
            return dt;
        }
Mohammad Mirmostafa
  • 1,720
  • 2
  • 16
  • 32
  • This does not answer the question. The querent is using System.Text.Json not Json.NET, and is deserializing a dictionary, not a data table. I also don't recommend swallowing and ignoring all exceptions. – dbc Mar 30 '22 at 18:22