当枚举键采用 kebab-case-lower 格式时,我无法从 JSON 字符串反序列化
Dictionary<Enum,decimal>
。我尝试定义 JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower)
,但它没有按预期工作。我在 .NET 8 中使用 C# 8 编译器
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var json = @"{
""enum-one"": 1.1
}";
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = {
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower)
}
};
var enumOne = JsonSerializer.Deserialize<TestEnum>(@"""enum-one""", options); // This works
var enumDictionary = JsonSerializer.Deserialize<Dictionary<TestEnum,decimal>>(json, options); // This will throw error
}
}
public enum TestEnum
{
EnumOne = 1,
EnumTwo = 2,
EnumThree = 3
}
未处理的异常。 System.Text.Json.JsonException:无法将 JSON 值转换为 TestEnum。路径:$.enum-one |行号:1 |字节位置内联:23。
代码演示:https://dotnetfiddle.net/XA0uPX
我做错了什么或遗漏了什么吗?
您遇到了 Microsoft 对 System.Text.Json 声明的、有意的限制。 来自文档:
对字典键使用命名策略
设置为非默认命名策略,键也将与 JSON 文件匹配。JsonSerializerOptions.DictionaryKeyPolicy
要确认此限制甚至适用于
Enum
字典键,您可以检查 EnumConverter<T>
的 参考源,其中包含以下注释:
// NB JsonSerializerOptions.DictionaryKeyPolicy is ignored on deserialization. // This is true for all converters that implement dictionary key serialization.
那么你有什么选择?
在 .NET 7 及更高版本中 Microsoft 在反序列化枚举值时实现了对命名策略的支持(请参阅issue #31619),因此他们显然有能力在考虑命名策略的情况下反序列化枚举字符串。 因此,您可以创建一个
JsonConverter
适配器,将呼叫从 ReadAsPropertyName()
转发到 Read()
:
public class DictionaryKeyJsonStringEnumConverter : JsonConverterFactory
{
readonly JsonStringEnumConverter enumConverter;
public DictionaryKeyJsonStringEnumConverter() : this(new JsonStringEnumConverter()) { }
public DictionaryKeyJsonStringEnumConverter(JsonNamingPolicy? namingPolicy = default, bool allowIntegerValues = true) : this(new JsonStringEnumConverter(namingPolicy, allowIntegerValues)) { }
public DictionaryKeyJsonStringEnumConverter(JsonStringEnumConverter enumConverter) =>
this.enumConverter = enumConverter ?? throw new ArgumentNullException(nameof(enumConverter));
public sealed override bool CanConvert(Type objectType) => enumConverter.CanConvert(objectType);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var innerEnumConverter = enumConverter.CreateConverter(typeToConvert, options);
if (innerEnumConverter == null)
return null;
return (JsonConverter?)Activator.CreateInstance(typeof(EnumConverterDecorator<>).MakeGenericType(typeToConvert), new object? [] { options, innerEnumConverter });
}
class EnumConverterDecorator<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
{
readonly JsonConverter<TEnum> innerConverter;
public EnumConverterDecorator(JsonSerializerOptions options, JsonConverter<TEnum> innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
public override bool CanConvert(Type typeToConvert) => innerConverter.CanConvert(typeToConvert);
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) =>
innerConverter.Write(writer, value, options);
public override void WriteAsPropertyName(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) =>
innerConverter.WriteAsPropertyName(writer, value, options);
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
innerConverter.Read(ref reader, typeToConvert, options);
public override TEnum ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// If we simply call Read() here then System.Text.Json will intentionally choke because reader.TokenType is JsonTokenType.PropertyName not JsonTokenType.String
// So we need to create a standalone JSON string literal, and read that.
var quotedPropertyName = reader.GetQuotedUtf8PropertyName(stackalloc byte[64]);
var valueReader = new Utf8JsonReader(quotedPropertyName, options.GetReaderOptions());
return this.Read(ref valueReader.MoveToContentAndAssert(), typeToConvert, options);
}
}
}
public static class JsonExtensions
{
internal static Span<byte> GetQuotedUtf8PropertyName(this ref readonly Utf8JsonReader reader, Span<byte> buffer)
{
if (reader.TokenType != JsonTokenType.PropertyName)
throw new NotImplementedException(string.Format("Invalid TokenType {0}", reader.TokenType));
ReadOnlySpan<byte> unquotedProperty = reader.HasValueSequence
? unquotedProperty = reader.ValueSequence.ToArray()
: reader.ValueSpan;
Span<byte> quotedProperty = unquotedProperty.Length + 2 <= buffer.Length
? buffer.Slice(0, 2 + unquotedProperty.Length) : new byte[2 + unquotedProperty.Length].AsSpan();
quotedProperty[0] = (byte)'"';
unquotedProperty.CopyTo(quotedProperty.Slice(1));
quotedProperty[quotedProperty.Length - 1] = (byte)'"';
return quotedProperty;
}
public static JsonReaderOptions GetReaderOptions(this JsonSerializerOptions? options) =>
options is null ? new () : new ()
{
AllowTrailingCommas = options.AllowTrailingCommas,
CommentHandling = options.ReadCommentHandling,
MaxDepth = options.MaxDepth
};
public static ref Utf8JsonReader MoveToContentAndAssert(ref this Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.None)
return ref reader.ReadAndAssert();
return ref reader;
}
public static ref Utf8JsonReader ReadAndAssert(ref this Utf8JsonReader reader) { if (!reader.Read()) { throw new JsonException(); } return ref reader; }
}
然后用它代替
JsonStringEnumConverter
,如下所示:
var json =
"""
{
"enum-one": 1.1,
"3": 3.3
}
""";
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = {
new DictionaryKeyJsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower)
}
};
var enumDictionary = JsonSerializer.Deserialize<Dictionary<TestEnum,decimal>>(json, options);
var newJson = JsonSerializer.Serialize(enumDictionary, options);
并且您的枚举字典应该成功往返。
备注:
JsonNamingPolicy
没有反转 UnconvertName(string)
的
ConvertName(string)
方法。 然而,由于枚举具有一组固定的值,因此转换器可以构建一个反向映射表——这实际上是在 .NET 7 中实现的。演示小提琴在这里。
JsonStringEnumMemberNameAttribute
直接应用于枚举值来避免对自定义转换器的需要,如下所示:
public enum TestEnum
{
[JsonStringEnumMemberName("enum-one")]
EnumOne = 1,
[JsonStringEnumMemberName("enum-two")]
EnumTwo = 2,
[JsonStringEnumMemberName("enum-three")]
EnumThree = 3
}
(不过,我还无法测试 .NET 9。)