Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

System.Text.Json: How do I specify a custom name for an enum value?

Using the System.Text.Json serializer capabilities in .NET Core, how can I specify a custom value for an enum value, similar to JsonPropertyName? For example:

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 } 
like image 271
Craig Smitham Avatar asked Nov 26 '19 22:11

Craig Smitham


People also ask

How to set JsonProperty name in c#?

To set the name of individual properties, use the [JsonPropertyName] attribute. The property name set by this attribute: Applies in both directions, for serialization and deserialization.

What is JSON property name?

The Jackson Annotation @JsonProperty is used on a property or method during serialization or deserialization of JSON. It takes an optional 'name' parameter which is useful in case the property name is different than 'key' name in JSON.

What is Stringenumconverter?

Converts an Enum to and from its name string value. Newtonsoft.Json. JsonConverter. Newtonsoft.Json.Converters.

What is JSON property in C#?

JsonPropertyAttribute indicates that a property should be serialized when member serialization is set to opt-in. It includes non-public properties in serialization and deserialization. It can be used to customize type name, reference, null, and default value handling for the property value.


1 Answers

This is not currently supported out of the box in .net-core-3.0. There is currently an open issue Support for EnumMemberAttribute in JsonConverterEnum #31081 requesting this functionality. In the interim, you will need to create your own JsonConverterFactory that serializes enums with custom value names specified by attributes.

If you need to round-trip an enum with custom value names you will need to create a generic converter + converter factory from scratch. This is somewhat involved in general as it is necessary to handle parsing of integer and string values, renaming of each component of a [Flags] enum value, and enums of all possible underlying types (byte, short, int, long, ulong etc).

JsonStringEnumMemberConverter from Macross.Json.Extensions appears to provide this functionality when the enum is decorated with [EnumMember(Value = "custom name")] attributes; install the package Macross.Json.Extensions and then do:

[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, } 

See the docs here for usage details.

Alternatively you could roll your own by using Json.NET's StringEnumConverter as a reference model.

If you only need to serialize an enum with custom value names this can be done more easily by creating a JsonConverterFactory that adapts JsonStringEnumConverter by constructing a customized JsonNamingPolicy for each enum type that looks for the presence of [EnumMember(Value = "xxx")] attributes on the enum's members, and if any are found, maps the member name to the attribute's value. (I chose EnumMember because this is the attribute supported by Newtonsoft.)

First, introduce the following converter:

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); } 

Then decorate your enum:

public enum Example  {   Trick,   Treat,   [EnumMember(Value = "Trick-Or-Treat")]    TrickOrTreat, } 

And use the converter standalone as follows:

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

To register the converter with asp.net core, see e.g. this answer to JsonConverter equivalent in using System.Text.Json by Mani Gandham.

Notes:

  • This approach only works for serialization because JsonConverterFactory ignores its naming policy during deserialization; see System.Text.Json: JsonStringEnumConverter ignores its JsonNamingPolicy during deserialization. #31619 for details.

  • In .Net Core 3.x the converter may not work as desired with [Flags] enums such as:

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

    Simple values like Example.TrickOrTreat are renamed properly, but composite values like Example.Trick | Example.TrickOrTreat are not. The result for the latter should be "Trick, Trick-Or-Treat" but is instead "Trick, TrickOrTreat".

    The cause of the problem is that the underlying JsonConverterEnum<T> for each specific enum type T calls ConvertName once with the constructed composite name rather than multiple times with each component of the composite name. If a workaround is required, in DictionaryLookupNamingPolicy.ConvertName() you could try splitting the incoming name into comma-separated components, remapping each component, then recombining the results.

    For comparison, Json.NET's StringEnumConverter calls the equivalent method NamingStrategy.ResolvePropertyName(string name) on every component of a composite flag value, which seems more correct.

    In .Net 5 this is fixed, see Issue #31622 for details.

Demo fiddle here.

like image 189
dbc Avatar answered Sep 17 '22 21:09

dbc