JSON.Net 默认情况下将枚举序列化为字典中的字符串 - 如何使其序列化为 int

本文关键字:序列化 字符串 int 何使其 字典 情况下 枚举 Net JSON 默认 | 更新日期: 2023-09-27 18:17:10

为什么我的序列化 JSON 最终为

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

即为什么枚举序列化为它们的值,但是当它们形成它们对字典的键时,它们被转换为它们的键?

如何使它们成为字典中的整数,为什么这不是默认行为?

我希望以下输出

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

我的代码:

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

JSON.Net 默认情况下将枚举序列化为字典中的字符串 - 如何使其序列化为 int

Gender枚举在用作属性值时序列化为其值,但在用作字典键时序列化为其字符串表示形式的原因如下:

  • 当用作属性值时 JSON.NET 序列化程序首先写入属性名称,然后写入属性值。对于您发布的示例,JSON.NET 将"Gender"写入属性名称(请注意,它写入了一个字符串(,然后尝试解析属性的值。属性的值是枚举类型,JSON.NET 将其处理为Int32,并写入枚举的数字表示形式

  • 序列化字典时,键将写入为属性名称,因此 JSON.NET 序列化程序写入枚举的字符串表示形式。如果在字典中切换键和值的类型(Dictionary<int, Gender>而不是Dictionary<Gender, int>,您将验证枚举是否将使用其Int32表示形式进行序列化。

若要通过发布的示例实现所需的内容,需要为 Dictionary 属性编写自定义JsonConverter。像这样:

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

并在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>();
    }
}

调用以下行进行序列化时:

var serializeObject = JsonConvert.SerializeObject(testClass);

您将获得所需的输出:

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

我经常发现自己面临这个问题,所以我做了一个 JsonConverter,它可以处理任何类型的字典,以 Enum 类型作为键:

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

注意:这不会处理Dictionnary<Enum, Dictionnary<Enum, T>

Ilija Dimod的答案涵盖了为什么会发生这种情况,但建议的转换器仅适用于这种特定情况。

这是一个可重用的转换器,它将枚举键格式化为它们的值,适用于任何Dictionary<,>/IDictionary<,>字段中的任何枚举键:

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

您可以在特定字段上启用它:

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

或者为所有带有枚举键的词典启用它:

JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
    Converters = new List<JsonConverter>
    {
        new DictionaryNumericEnumKeysConverter()
    }
};

如果你问为什么这是默认行为作为谨慎的问题,IMO至少有几个很好的理由,但两者最终都与互操作性有关,即,你的JSON数据与你自己的和/或使用另一种语言和/或不共享你的C#代码的另一个系统交换的可能性。

首先,如果您插入更多数字并且自己不指定数字,则 C# 中的enum数字值在不同的程序集版本中可能会更改。当然,您在此处指定了数字,但许多人没有。此外,即使您确实指定了数字,您现在也会将这些值作为 API 合约的一部分始终提交,或者进行重大更改。即使此 JSON 数据的唯一使用者是您自己的代码,除非您使用某种基于 C# 的自动 Typescript 生成器(我这样做并且应该更频繁地这样做!!(,否则如果您想更改这些数字,您至少需要更新两个位置。因此,通过使用enum名称而不是数字值进行序列化,使用者可以忽略数值,并且在enum数字更改时无法中断。

其次,字符串值对消费者更友好。任何查看原始JSON数据的人都可能能够很好地理解字典的内容,而无需任何文档。更好的是,消费者不必在自己的代码中独立跟踪每个键的数字值,这只会是他们出错的另一个机会。

因此,正如其他人所指出的,如果需要,您可以更改此行为,但是就选择捕获大多数人的做法和 JSON 背后的原始意图的默认行为而言,这似乎是正确的方法。