4

Is it possible to serialize (and deserialize) a dictionary as an array with System.Text.Json?

Instead of { "hello": "world" } I would need my dictionary serialized as { "key": "hello", "value": "world" } preferably without having to set attributes on the dictionary property of my class.

Using newtonsoft.json it was possible this way:

class DictionaryAsArrayResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        if (objectType.GetInterfaces().Any(i => i == typeof(IDictionary) || 
           (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>))))
        {
            return base.CreateArrayContract(objectType);
        }

        return base.CreateContract(objectType);
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
brechtvhb
  • 1,029
  • 2
  • 13
  • 26

2 Answers2

5

You can do this using a JsonConverterFactory that manufactures a specific JsonConverter<T> for every dictionary type that you want to serialize as an array. Here is one such converter that works for every class that implements IDictionary<TKey, TValue>:

public class DictionaryConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert.IsClass && typeToConvert.GetDictionaryKeyValueType() != null && typeToConvert.GetConstructor(Type.EmptyTypes) != null;
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var keyValueTypes = typeToConvert.GetDictionaryKeyValueType();
        var converterType = typeof(DictionaryAsArrayConverter<,,>).MakeGenericType(typeToConvert, keyValueTypes.Value.Key, keyValueTypes.Value.Value);
        return (JsonConverter)Activator.CreateInstance(converterType);
    }
}

public class DictionaryAsArrayConverter<TKey, TValue> : DictionaryAsArrayConverter<Dictionary<TKey, TValue>, TKey, TValue>
{
}

public class DictionaryAsArrayConverter<TDictionary, TKey, TValue> : JsonConverter<TDictionary> where TDictionary : class, IDictionary<TKey, TValue>, new()
{
    struct KeyValueDTO
    {
        public TKey Key { get; set; }
        public TValue Value { get; set; }
    }

    public override TDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var list = JsonSerializer.Deserialize<List<KeyValueDTO>>(ref reader, options);
        if (list == null)
            return null;
        var dictionary = typeToConvert == typeof(Dictionary<TKey, TValue>) ? (TDictionary)(object)new Dictionary<TKey, TValue>(list.Count) : new TDictionary();
        foreach (var pair in list)
            dictionary.Add(pair.Key, pair.Value);
        return dictionary;
    }

    public override void Write(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value.Select(p => new KeyValueDTO { Key = p.Key, Value = p.Value }), options);
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type.IsInterface)
            return new[] { type }.Concat(type.GetInterfaces());
        else
            return type.GetInterfaces();
    }

    public static KeyValuePair<Type, Type>? GetDictionaryKeyValueType(this Type type)
    {
        KeyValuePair<Type, Type>? types = null;
        foreach (var pair in type.GetDictionaryKeyValueTypes())
        {
            if (types == null)
                types = pair;
            else
                return null;
        }
        return types;
    }

    public static IEnumerable<KeyValuePair<Type, Type>> GetDictionaryKeyValueTypes(this Type type)
    {
        foreach (Type intType in type.GetInterfacesAndSelf())
        {
            if (intType.IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
            {
                var args = intType.GetGenericArguments();
                if (args.Length == 2)
                    yield return new KeyValuePair<Type, Type>(args[0], args[1]);
            }
        }
    }
}

Then add the factory to JsonSerializerOptions.Converters locally as follows:

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

var json = JsonSerializer.Serialize(dictionary, options);

var dictionary2 = JsonSerializer.Deserialize<TDictionary>(json, options);

Or globally in ASP.NET Core as shown in How to set json serializer settings in asp.net core 3?:

services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.Converters.Add(new DictionaryConverterFactory());
    options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});

The underlying individual converter DictionaryAsArrayConverter<TKey, TValue> can also be used directly if you only want to serialize certain dictionary types as arrays.

Notes:

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • I think calling `Dictionary.ToList()` could be quicker and simpler, but I like the deep customizations you demonstrate – Josh E Jan 08 '20 at 19:19
  • @JoshE - returning a list makes sense when the dictionary is the root object, but OP's question states that it's a *property of my class*, and implies that OP doesn't want to modify the model. In such a situation a converter factory is appropriate. Also, as noted in notes, `JsonSerializer` currently doesn't respect `PropertyNamingPolicy` for `KeyValuePair` so imply returning `ToList()` will result in incorrect casing. – dbc Jan 08 '20 at 19:31
  • fair enough on both points, I inferred too much from the way the OP asked their question – Josh E Jan 08 '20 at 21:08
1

If you want to keep it short and simple, you could consider projection via anonymous type:

var dictionary = new Dictionary<string, string>();
dictionary.Add("hello", "world");
dictionary.Add("how", "are you?");

var o = JsonSerializer.Serialize(dictionary.Select(x => new { key = x.Key, value = x.Value }));
// [{"key":"hello","value":"world"},{"key":"how","value":"are you?"}]

ed: of course, that's just if your feeling masochistic. If all you want is to just get the job done, just call .ToList()

JsonSerializer.Serialize(dictionary.ToList());
// [{"Key":"hello","Value":"world"},{"Key":"how","Value":"are you?"}]
Josh E
  • 7,390
  • 2
  • 32
  • 44