Dictionary<Enum,decimal> System.Text.Json 反序列化错误

问题描述 投票:0回答:1

当枚举键采用 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

我做错了什么或遗漏了什么吗?

c# .net-8.0 system.text.json c#-8.0
1个回答
0
投票

您遇到了 Microsoft 对 System.Text.Json 声明的、有意的限制。 来自文档

对字典键使用命名策略

字典键的命名策略仅适用于序列化。如果反序列化字典,即使您将

JsonSerializerOptions.DictionaryKeyPolicy
设置为非默认命名策略,键也将与 JSON 文件匹配。

要确认此限制甚至适用于

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 中实现的。

演示小提琴在这里

在 .NET 9 中,可以通过将

JsonStringEnumMemberNameAttribute
直接应用于枚举值来避免对自定义转换器的需要,如下所示:

public enum TestEnum
{
    [JsonStringEnumMemberName("enum-one")]
    EnumOne = 1,
    [JsonStringEnumMemberName("enum-two")]
    EnumTwo = 2,
    [JsonStringEnumMemberName("enum-three")]
    EnumThree = 3
}

(不过,我还无法测试 .NET 9。)

© www.soinside.com 2019 - 2024. All rights reserved.