为什么我的序列化 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>();
}
}
}
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 背后的原始意图的默认行为而言,这似乎是正确的方法。