5

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);
    }
}

2 Answers 2

7

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.

Sign up to request clarification or add additional context in comments.

3 Comments

I think calling Dictionary.ToList() could be quicker and simpler, but I like the deep customizations you demonstrate
@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<TKey, TValue> so imply returning ToList() will result in incorrect casing.
fair enough on both points, I inferred too much from the way the OP asked their question
0

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?"}]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.