System.Text.Json:如何为枚举值指定自定义名称?

Posted

技术标签:

【中文标题】System.Text.Json:如何为枚举值指定自定义名称?【英文标题】:System.Text.Json: How do I specify a custom name for an enum value? 【发布时间】:2019-11-26 22:00:00 【问题描述】:

使用 .NET Core 中的 System.Text.Json 序列化程序功能,如何为枚举值指定自定义值,类似于 JsonPropertyName?例如:

public enum Example 
  Trick, 
  Treat, 
  [JsonPropertyName("Trick-Or-Treat")] // Error: Attribute 'JsonPropertyName' is not valid on this declaration type. It is only valid on 'property, indexer' declarations.
   TrickOrTreat

【问题讨论】:

不支持通过.net-core-3.0 中的属性,请参阅Support for EnumMemberAttribute in JsonConverterEnum #41578。 可能带有JsonStringEnumConverter 你需要使用System.Text.Json吗?还是改用 Newtonsoft.Json 是否适合您? 【参考方案1】:

.net-core-3.0 目前不支持此功能。目前有一个未解决的问题 Support for EnumMemberAttribute in JsonConverterEnum #31081 要求使用此功能。在此期间,您将需要创建自己的 JsonConverterFactory 来序列化具有由属性指定的自定义值名称的枚举。

如果您需要往返一个带有自定义值名称的枚举,您需要从头开始创建一个通用转换器 + 转换器工厂。这在一般情况下有些涉及,因为有必要处理整数和字符串值的解析,重命名[Flags] 枚举值的每个组件,以及所有可能的underlying types 的枚举(byteshort,@987654352 @、longulong 等)。

当枚举用[EnumMember(Value = "custom name")] 属性修饰时,来自Macross.Json.ExtensionsJsonStringEnumMemberConverter 似乎提供了此功能;安装包Macross.Json.Extensions 然后执行:

[JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumMemberConverter))]  // This custom converter was placed in a system namespace.
public enum Example 

  Trick,
  Treat,
  [EnumMember(Value = "Trick-Or-Treat")]
   TrickOrTreat,

有关使用详情,请参阅文档here。

或者,您也可以使用 Json.NET 的 StringEnumConverter 作为参考模型来创建自己的模型。

如果您只需要序列化具有自定义值名称的枚举,这可以通过创建一个适应 JsonStringEnumConverterJsonConverterFactory 来更轻松地完成,方法是构造一个自定义的JsonNamingPolicy 为每个 enum 类型查找枚举成员上是否存在 [EnumMember(Value = "xxx")] 属性,如果找到,则将成员名称映射到属性值。 (我选择EnumMember是因为这是Newtonsoft支持的属性。)

首先介绍如下转换器:

public class CustomJsonStringEnumConverter : JsonConverterFactory

    private readonly JsonNamingPolicy namingPolicy;
    private readonly bool allowIntegerValues;
    private readonly JsonStringEnumConverter baseConverter;

    public CustomJsonStringEnumConverter() : this(null, true)  

    public CustomJsonStringEnumConverter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true)
    
        this.namingPolicy = namingPolicy;
        this.allowIntegerValues = allowIntegerValues;
        this.baseConverter = new JsonStringEnumConverter(namingPolicy, allowIntegerValues);
    
    
    public override bool CanConvert(Type typeToConvert) => baseConverter.CanConvert(typeToConvert);

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    
        var query = from field in typeToConvert.GetFields(BindingFlags.Public | BindingFlags.Static)
                    let attr = field.GetCustomAttribute<EnumMemberAttribute>()
                    where attr != null
                    select (field.Name, attr.Value);
        var dictionary = query.ToDictionary(p => p.Item1, p => p.Item2);
        if (dictionary.Count > 0)
        
            return new JsonStringEnumConverter(new DictionaryLookupNamingPolicy(dictionary, namingPolicy), allowIntegerValues).CreateConverter(typeToConvert, options);
        
        else
        
            return baseConverter.CreateConverter(typeToConvert, options);
        
    


public class JsonNamingPolicyDecorator : JsonNamingPolicy 

    readonly JsonNamingPolicy underlyingNamingPolicy;
    
    public JsonNamingPolicyDecorator(JsonNamingPolicy underlyingNamingPolicy) => this.underlyingNamingPolicy = underlyingNamingPolicy;

    public override string ConvertName (string name) => underlyingNamingPolicy == null ? name : underlyingNamingPolicy.ConvertName(name);


internal class DictionaryLookupNamingPolicy : JsonNamingPolicyDecorator 

    readonly Dictionary<string, string> dictionary;

    public DictionaryLookupNamingPolicy(Dictionary<string, string> dictionary, JsonNamingPolicy underlyingNamingPolicy) : base(underlyingNamingPolicy) => this.dictionary = dictionary ?? throw new ArgumentNullException();
    
    public override string ConvertName (string name) => dictionary.TryGetValue(name, out var value) ? value : base.ConvertName(name);

然后装饰你的enum:

public enum Example 

  Trick,
  Treat,
  [EnumMember(Value = "Trick-Or-Treat")]
   TrickOrTreat,

并按如下方式独立使用转换器:

var options = new JsonSerializerOptions

    Converters =  new CustomJsonStringEnumConverter() ,
    WriteIndented = true,
;
var json = JsonSerializer.Serialize(values, options);

要向 asp.net core 注册转换器,请参见例如this answer 到 JsonConverter equivalent in using System.Text.Json 由Mani Gandham。

注意事项:

这种方法只适用于序列化,因为JsonConverterFactory 在反序列化过程中会忽略其命名策略;详情请参阅System.Text.Json: JsonStringEnumConverter ignores its JsonNamingPolicy during deserialization. #31619

在 .Net Core 3.x 中,转换器可能无法使用 [Flags] 枚举,例如:

[Flags]
public enum Example 

  Trick = (1<<0),
  Treat = (1<<1),
  [EnumMember(Value = "Trick-Or-Treat")]
   TrickOrTreat = (1<<2),

Example.TrickOrTreat 这样的简单值可以正确重命名,但像Example.Trick | Example.TrickOrTreat 这样的复合值则不能。后者的结果应该是"Trick, Trick-Or-Treat",而是"Trick, TrickOrTreat"

问题的原因是每个特定枚举类型T 的底层JsonConverterEnum&lt;T&gt; 使用构造的复合名称调用ConvertName 一次,而不是使用复合名称的每个组件多次调用。如果需要解决方法,在DictionaryLookupNamingPolicy.ConvertName() 中,您可以尝试将传入名称拆分为逗号分隔的组件,重新映射每个组件,然后重新组合结果。

为了比较,Json.NET 的 StringEnumConverter 对复合标志值的每个组件调用等效方法 NamingStrategy.ResolvePropertyName(string name),这似乎更正确。

在 .Net 5 中,此问题已修复,详情请参阅 Issue #31622。

演示小提琴here.

【讨论】:

看来这将是 v.5 中的一个功能。在过去,这个库有效:nuget.org/packages/Macross.Json.Extensions 也许这个库也能帮到你:github.com/StefH/System.Text.Json.EnumExtensions 仅适用于序列化 :( 不能以类似的方式反序列化 这是一个关于如何使用 .NET Core 3.1 实现目标的好答案,我将接受一个为 .NET 5 提供简洁示例的新答案,包括序列化和反序列化 @CraigSmitham - 不幸的是,Support for EnumMemberAttribute in JsonConverterEnum #31081 在 .Net 5.0 中仍然开放。据我所知,唯一得到修复的与枚举相关的事情是System.Text.Json: Naming policies are not applied consistently to [Flags] enum values by JsonStringEnumConverter. #31622。可以找到所有 .Net 5 System.Text.Json 改进的综述 here【参考方案2】:

如果是 .NET 5:

services.AddControllers()
    .AddJsonOptions(opts => opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));

【讨论】:

请添加一些解释,为什么您认为您提出的解决方案可能对 OP 有所帮助。 这个答案是错误的。它不能解决具有自定义名称的枚举的反序列化问题 它不会反序列化。【参考方案3】:

在.net-core-5.0 和asp.net-core-5.0 中,Microsoft 已通过JsonStringEnumConverter Class 添加了对反序列化枚举的支持。

像这样装饰你的枚举值:

using System.Runtime.Serialization;
public enum VipStatus

    [EnumMember(Value = @"IS_VIP")]
    VIP = 1,

    [EnumMember(Value = @"IS_NOT_VIP")]
    NonVIP = 2,

给定一个像这样的课程:

class MyClass 
    public VipStatus MyVipStatus  get; set; 

您可以使用 JsonStringEnumConverter 内联来序列化类的实例,如下所示:

using System.Text.Json;
using System.Text.Json.Serialization;
// ...

var myObjectWithEnums = new MyClass()

    MyVipStatus = VipStatus.NonVIP
;

var options = new JsonSerializerOptions();

// Configures serialization to allow strings to be accepted and auto-converted to enum values.
options.Converters.Add(new JsonStringEnumConverter());

var json = JsonSerializer.Serialize(myObjectWithEnums, options);
// serialized output is:  "myVipStatus": "IS_NOT_VIP"

如果您使用的是 ASP.NET Core 5,那么您可以在启动时配置应用程序以使用 JsonStringEnumConverter 序列化所有传入请求:

public async void ConfigureServices(IServiceCollection services) 
    // ...
    services
        .AddControllers()
        .AddJsonOptions(options => 
            // Configures serialization to allow strings to be accepted and auto-converted to enum values.
            options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        
    // ...
);

更多阅读:How to serialize and deserialize (marshal and unmarshal) JSON in .NET Core。如果您在 ASP.NET 中工作,那么这也很有趣:Web defaults for JsonSerializerOptions。

【讨论】:

我在文档或其他地方找不到任何证据表明这会导致 System.Text.Json 尊重 EnumMember。看起来链接的 github 问题仍然开放。 我刚刚用 net5.0 试过这个,虽然它确实序列化,但它不尊重属性值,因此 Enum 名称被序列化。在您的情况下,输出是 "NonVIP" 而不是 "IS_NOT_VIP"

以上是关于System.Text.Json:如何为枚举值指定自定义名称?的主要内容,如果未能解决你的问题,请参考以下文章

.NET Core/System.Text.Json:枚举和添加/替换 json 属性/值

对于将 HTTP/JSON 转码为 gRPC,如何为枚举指定查询参数

System.Text.Json:当json配置具有通用JsonStringEnumConverter时,如何将单个枚举序列化为数字[重复]

在 Django 中,如何为 IntegerChoices 枚举定义字符串值?

如何使用 System.Text.Json 序列化/反序列化非枚举类型的嵌套字典?

[小技巧]C#中如何为枚举类型添加描述方法