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

Cra*_*ham 22 .net c# .net-core .net-core-3.0 system.text.json

使用.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
}
Run Code Online (Sandbox Code Playgroud)

dbc*_*dbc 25

目前在中不支持开箱即用。当前有一个开放问题Support for EnumMemberAttribute in JsonConverterEnum #31081请求此功能。在此期间,您将需要创建自己的JsonConverterFactory序列化具有由属性指定的自定义值名称的枚举。

如果您需要使用自定义值名称往返枚举,则需要从头开始创建通用转换器 + 转换器工厂。这在一定程度上参与一般,因为它是必要的处理解析整数和字符串值的,重命名的各成分的[Flags]枚举值,以及所有可能的枚举底层类型byteshortintlongulong等等)。

JsonStringEnumMemberConverterMacross.Json.Extensions当使用[EnumMember(Value = "custom name")]属性修饰枚举时,from似乎提供了此功能;安装软件包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,
}
Run Code Online (Sandbox Code Playgroud)

有关使用详细信息,请参阅此处的文档。

或者,您可以使用 Json.NETStringEnumConverter作为参考模型来推出自己的模型。

如果您只需要使用自定义值名称序列化一个枚举,这可以通过创建一个JsonConverterFactory适应JsonStringEnumConverter的更轻松地完成,该适应通过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);
}
Run Code Online (Sandbox Code Playgroud)

然后装饰你的enum

public enum Example 
{
  Trick,
  Treat,
  [EnumMember(Value = "Trick-Or-Treat")]
   TrickOrTreat,
}
Run Code Online (Sandbox Code Playgroud)

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

var options = new JsonSerializerOptions
{
    Converters = { new CustomJsonStringEnumConverter() },
    WriteIndented = true,
};
var json = JsonSerializer.Serialize(values, options);
Run Code Online (Sandbox Code Playgroud)

要注册使用asp.net核心转换器,例如见这个答案JsonConverter相当于使用System.Text.Json摩尼Gandham

笔记:

  • 这种方法只适用于序列化,因为JsonConverterFactory在反序列化过程中忽略了它的命名策略;请参阅System.Text.Json:JsonStringEnumConverter 在反序列化期间忽略其 JsonNamingPolicy。#31619详情。

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

    [Flags]
    public enum Example 
    {
      Trick = (1<<0),
      Treat = (1<<1),
      [EnumMember(Value = "Trick-Or-Treat")]
       TrickOrTreat = (1<<2),
    }
    
    Run Code Online (Sandbox Code Playgroud)

    像这样的简单值Example.TrickOrTreat被正确重命名,但像这样的复合值Example.Trick | Example.TrickOrTreat则没有。后者的结果应该是"Trick, Trick-Or-Treat"but is "Trick, TrickOrTreat"

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

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

    在 .Net 5 中,这是固定的,有关详细信息,请参阅问题 #31622

演示小提琴在这里

  • 也许这个库也可以帮助你:https://github.com/StefH/System.Text.Json.EnumExtensions (3认同)
  • @CraigSmitham - 不幸的是 [JsonConverterEnum #31081 中对 EnumMemberAttribute 的支持](https://github.com/dotnet/runtime/issues/31081) 在 .Net 5.0 中仍然开放。据我所知,唯一修复的与枚举相关的问题是 [System.Text.Json:JsonStringEnumConverter 的命名策略并未一致地应用于“[Flags]”枚举值。#31622](https://github.com/dotnet/runtime/issues/31622)。所有 .Net 5 `System.Text.Json` 改进的汇总可以在[此处](https://github.com/dotnet/runtime/issues/41313)找到 (3认同)
  • 愿上帝保佑你得到这个答案。在花费太多时间查看 GitHub 功能请求后,Nuget 包成功了 (3认同)
  • 看来这将是 v.5 中的一个功能。在同一时间,这个库可以工作:https://www.nuget.org/packages/Macross.Json.Extensions/ (2认同)