14

Why does my serialized JSON end up as

{"Gender":1,"Dictionary":{"Male":100,"Female":200}}

i.e. why do the enums serialize to their value, but when they form they key to the dictionary they are converted to their key?

How do I make them be ints in the dictionary, and why isn't this the default behaviour?

I'd expect the following output

{"Gender":1,"Dictionary":{"0":100,"1":200}}

My code:

    public void foo()
    {
        var testClass = new TestClass();
        testClass.Gender = Gender.Female;
        testClass.Dictionary.Add(Gender.Male, 100);
        testClass.Dictionary.Add(Gender.Female, 200);

        var serializeObject = JsonConvert.SerializeObject(testClass);

        // serializeObject == {"Gender":1,"Dictionary":{"Male":100,"Female":200}}
    }

    public enum Gender
    {
        Male = 0,
        Female = 1
    }

    public class TestClass
    {
        public Gender Gender { get; set; }
        public IDictionary<Gender, int> Dictionary { get; set; }

        public TestClass()
        {
            this.Dictionary = new Dictionary<Gender, int>();
        }
    }
}
halfer
  • 19,824
  • 17
  • 99
  • 186
Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231
  • possible duplicate of [How to prevent Json.NET converting enum to string?](http://stackoverflow.com/questions/24985148/how-to-prevent-json-net-converting-enum-to-string) – Brian Rogers Aug 08 '14 at 14:43

4 Answers4

9

The reason why Gender enum is serialized to its value when used as property value, but it is serialized to its string representation when used as dictionary key is the following:

  • When used as property value JSON.NET serializer first writes the property name and after that the property value. For the example you posted, JSON.NET will write "Gender" as property name (notice that it writes a string), than will try to resolve the value of the property. The value of the property is of type enum which JSON.NET handles as Int32 and it writes the number representation of the enum

  • When serializing the dictionary, the keys are written as property names, so the JSON.NET serializer writes the string representation of the enum. If you switch the types of the keys and values in the dictionary (Dictionary<int, Gender> instead of Dictionary<Gender, int>, you'll verify that the enum will be serialized with its Int32 representation.

To achieve what you want with the example you posted, you'll need to write custom JsonConverter for the Dictionary property. Something like this:

public class DictionaryConverter : JsonConverter
{

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var dictionary = (Dictionary<Gender, int>) value;

        writer.WriteStartObject();

        foreach (KeyValuePair<Gender, int> pair in dictionary)
        {
            writer.WritePropertyName(((int)pair.Key).ToString());
            writer.WriteValue(pair.Value);
        }

        writer.WriteEndObject();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jObject = JObject.Load(reader);

        var maleValue = int.Parse(jObject[((int) Gender.Male).ToString()].ToString());
        var femaleValue = int.Parse(jObject[((int)Gender.Female).ToString()].ToString());

        (existingValue as Dictionary<Gender, int>).Add(Gender.Male, maleValue);
        (existingValue as Dictionary<Gender, int>).Add(Gender.Female, femaleValue);

        return existingValue;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof (IDictionary<Gender, int>) == objectType;
    }
}

and decorate the property in the TestClass:

public class TestClass
{
    public Gender Gender { get; set; }
    [JsonConverter(typeof(DictionaryConverter))]
    public IDictionary<Gender, int> Dictionary { get; set; }

    public TestClass()
    {
        this.Dictionary = new Dictionary<Gender, int>();
    }
}

When calling the following line for serialization:

var serializeObject = JsonConvert.SerializeObject(testClass);

you'll get the desired output:

{"Gender":1,"Dictionary":{"0":100,"1":200}}
Ilija Dimov
  • 5,221
  • 7
  • 35
  • 42
6

I often find myself facing this issue so I did a JsonConverter that can handle any kind of dictionnary with an Enum type as key:

public class DictionaryWithEnumKeyConverter<T, U> : JsonConverter where T : System.Enum
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var dictionary = (Dictionary<T, U>)value;

        writer.WriteStartObject();

        foreach (KeyValuePair<T, U> pair in dictionary)
        {
            writer.WritePropertyName(Convert.ToInt32(pair.Key).ToString());
            serializer.Serialize(writer, pair.Value);
        }

        writer.WriteEndObject();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var result = new Dictionary<T, U>();
        var jObject = JObject.Load(reader);

        foreach (var x in jObject)
        {
            T key = (T) (object) int.Parse(x.Key); // A bit of boxing here but hey
            U value = (U) x.Value.ToObject(typeof(U));
            result.Add(key, value);
        }

        return result;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(IDictionary<T, U>) == objectType;
    }
}

NB: This will not handle Dictionnary<Enum, Dictionnary<Enum, T>

dekajoo
  • 2,024
  • 1
  • 25
  • 36
0

The answer from Ilija Dimov covers why it happens, but the suggested converter only works for this specific case.

Here's a reusable converter which formats enum keys as their value, for any enum key in any Dictionary<,>/IDictionary<,> field:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Newtonsoft.Json;

/// <summary>A Json.NET converter which formats enum dictionary keys as their underlying value instead of their name.</summary>
public class DictionaryNumericEnumKeysConverter : JsonConverter
{
    public override bool CanRead => false; // the default converter handles numeric keys fine
    public override bool CanWrite => true;

    public override bool CanConvert(Type objectType)
    {
        return this.TryGetEnumType(objectType, out _);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        throw new NotSupportedException($"Reading isn't implemented by the {nameof(DictionaryNumericEnumKeysConverter)} converter."); // shouldn't be called since we set CanRead to false
    }

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        // handle null
        if (value is null)
        {
            writer.WriteNull();
            return;
        }

        // get dictionary & key type
        if (value is not IDictionary dictionary || !this.TryGetEnumType(value.GetType(), out Type? enumType))
            throw new InvalidOperationException($"Can't parse value type '{value.GetType().FullName}' as a supported dictionary type."); // shouldn't be possible since we check in CanConvert
        Type enumValueType = Enum.GetUnderlyingType(enumType);

        // serialize
        writer.WriteStartObject();
        foreach (DictionaryEntry pair in dictionary)
        {
            writer.WritePropertyName(Convert.ChangeType(pair.Key, enumValueType).ToString()!);
            serializer.Serialize(writer, pair.Value);
        }
        writer.WriteEndObject();
    }

    /// <summary>Get the enum type for a dictionary's keys, if applicable.</summary>
    /// <param name="objectType">The possible dictionary type.</param>
    /// <param name="keyType">The dictionary key type.</param>
    /// <returns>Returns whether the <paramref name="objectType"/> is a supported dictionary and the <paramref name="keyType"/> was extracted.</returns>
    private bool TryGetEnumType(Type objectType, [NotNullWhen(true)] out Type? keyType)
    {
        // ignore if type can't be dictionary
        if (!objectType.IsGenericType || objectType.IsValueType)
        {
            keyType = null;
            return false;
        }

        // ignore if not a supported dictionary
        {
            Type genericType = objectType.GetGenericTypeDefinition();
            if (genericType != typeof(IDictionary<,>) && genericType != typeof(Dictionary<,>))
            {
                keyType = null;
                return false;
            }
        }

        // extract key type
        keyType = objectType.GetGenericArguments().First();
        if (!keyType.IsEnum)
            keyType = null;

        return keyType != null;
    }
}

You can enable it on a specific field:

[JsonConverter(typeof(DictionaryNumericEnumKeysConverter))]
public IDictionary<Gender, int> Dictionary { get; set; }

Or enable it for all dictionaries with enum keys:

JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
    Converters = new List<JsonConverter>
    {
        new DictionaryNumericEnumKeysConverter()
    }
};
halfer
  • 19,824
  • 17
  • 99
  • 186
Pathoschild
  • 4,636
  • 2
  • 23
  • 25
  • 1
    (Minor point of order: answers on a question do not appear in a guaranteed order, so referring to the "top answer" doesn't mean much. On this page, for me, your answer is the top one! I have made an edit to point to the answer I think you meant.) – halfer Apr 15 '22 at 21:07
0

If you're asking why this is the default behavior as a matter of compu-prudence, there are at least a couple of really good reasons IMO, but both ultimately have to do with interoperability, i.e., the likelihood that your JSON data is getting exchanged with another system outside your own and/or that uses another language and/or doesn't share your C# code.

First, enum number values in C# can change in different assembly versions if you insert more and don't specify numbers yourself. Granted you specify the numbers here, but many people do not. Plus, even if you do specify the numbers, you're now committing to these values as part of your API contract for all time, or else make a breaking change. And even if the only consumer of this JSON data is your own code, unless you use some kind of automatic Typescript generator based on your C# (which I do and should be done more often!!) you have at least two places you'd need to update if you want to change these numbers. So by serializing using the enum name as the key rather than the number value, consumers can remain oblivious to the numeric values and have no possibility of breaking when the enum numbers change.

Second, the string values are just more user friendly to consumers. Anyone looking at the raw JSON data would likely be able to get a good understanding of the contents of the dictionary without the need for any documentation. Better still, the consumer doesn't have to be keeping independent track of the number values for each key in their own code, which would just be another opportunity for error on their end.

So as others noted, you can change this behavior if you want, but in terms of picking a default behavior that captures the practice of the most people and the original intent behind JSON, this seems like the right approach.

Emperor Eto
  • 2,456
  • 2
  • 18
  • 32