Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for EnumMemberAttribute in JsonConverterEnum #31081

Closed
kolkinn opened this issue Oct 6, 2019 · 12 comments
Closed

Support for EnumMemberAttribute in JsonConverterEnum #31081

kolkinn opened this issue Oct 6, 2019 · 12 comments
Labels
area-System.Text.Json enhancement Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@kolkinn
Copy link

kolkinn commented Oct 6, 2019

Hi, I wonder if supporting EnumMemberAttribute in JsonConverterEnum is planned and if so what the ETA is?

Json.NET supports EnumMemberAttribute to customize enum name values. It is a well used feature.

You don't need it in 3.0, but you will get a lot of requests for customizing enum names. Design for adding it in the future.

Originally posted by @JamesNK in dotnet/corefx#38702 (comment)

@ericstj
Copy link
Member

ericstj commented Oct 28, 2019

/cc @steveharter

We should discuss if we want to support this and other attributes from System.Runtime.Serialization before adding them.

@ericstj
Copy link
Member

ericstj commented Nov 12, 2019

The basic scenario here is to provide control of the value names used for enum serialization. DCS and Json.NET use EnumMemberAttribute, XML uses XmlEnumAttribute.

@CodeBlanch
Copy link
Contributor

For anyone blocked by this, here's a NuGet package with a converter (JsonStringEnumMemberConverter) we can use ahead of the 5.0 drop: Macross.Json.Extensions

Supports nullable enum types and adds EnumMemberAttribute support.

@layomia
Copy link
Contributor

layomia commented May 26, 2020

From @AraHaan in #36931:

I personally think that EnumMember should be supported, many users try to use it with the JsonStringEnumConverter that comes shipped with System.Text.Json itself. Because of this they can run into an System.Text.Json.JsonException from doing such which sucks.

Take a look at the draft pr that is on hold because this is 1 of the issues they face when trying to eradicate newtonsoft.json: RehanSaeed/Schema.NET#100

Also take a look at a version of my code which did that, then I have to now struggle trying to get my stuff to compile because now I got to create a string extension method that could allow null strings somehow and then filter it to the enum values accordingly manually which also sucks.
https://paste.mod.gg/giwezarimo.cs
https://paste.mod.gg/nuqofuwamu.cs
https://paste.mod.gg/gunoqemufu.cs

You can literally use this code as a minimal example and as you can tell my code will not compile as this stuff is lacking. Note: I only made this change because of that pull request above I seen that it used EnumMember and that it used a converter I did not really know about so I thought "Wait a second so my manual converters is not needed at all?"

jmaine added a commit to jmaine/runtime that referenced this issue May 27, 2020
…correct attribute usage. Fix build issues on some platforms. (dotnet#31081)
jmaine added a commit to jmaine/runtime that referenced this issue May 27, 2020
jmaine added a commit to jmaine/runtime that referenced this issue May 27, 2020
jmaine added a commit to jmaine/runtime that referenced this issue May 27, 2020
jmaine added a commit to jmaine/runtime that referenced this issue May 27, 2020
jmaine added a commit to jmaine/runtime that referenced this issue May 28, 2020
@jmaine
Copy link
Contributor

jmaine commented May 28, 2020

Here is the new pull request for this issue: #37113.

@jmaine
Copy link
Contributor

jmaine commented May 28, 2020

Here is the proposed API to fix this issue. EnumMemberAttribute was considered, but the serializer prefers custom/dedicated attributes. This proposed API takes this into account and proposes a new attribute for this purpose.

Note: Pinging @layomia as this person expressed interested in this proposal.

Rationale and Usage

This may be a nice-to-have; however, as @mikaelkolkinn states, it is a blocker for migrating off of the Newtonsoft Json serializer/deserializer for some customers.

Proposed API

The proposed attribute would be the following. It would permit the overriding of any JsonNamingPolicy provided in the serializer.

namespace System.Text.Json.Serialization
{
    /// <summary>
    /// Specifies the enum member that is present in the JSON when serializing and deserializing.
    /// This overrides any naming policy specified by <see cref="JsonNamingPolicy"/>.
    /// </summary>
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public sealed class JsonStringEnumMemberAttribute : JsonAttribute
    {
        /// <summary>
        /// Initializes a new instance of <see cref="JsonStringEnumMemberAttribute"/> with the specified enum member name.
        /// </summary>
        /// <param name="name">The name of the enum member.</param>
        public JsonStringEnumMemberAttribute(string name)
        {
            Name = name;
        }

        /// <summary>
        /// The name of the enum member.
        /// </summary>
        public string Name { get; }
    }
}

The usage of this attribute would be the following.

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum MyCustomJsonStringEnumMemberEnum
{
            [System.Text.Json.Serialization.JsonStringEnumMemberAttribute("one_")]
            One,
            [System.Text.Json.Serialization.JsonStringEnumMemberAttribute("two_")]
            Two,
            [System.Text.Json.Serialization.JsonStringEnumMemberAttribute(null)]
            Null
}

The result of each one of these example enum members when serialized will be the following json values: "one_", "two_", null

Details

The serializer will respect this new attribute on both serialization and deserialization. The serializer will allow any string or null as permissible values in the attribute and will convert it from/to the specified enum member where the attribute is attached to.

This attribute will also override any JsonNamingPolicy provided in the serializer.

Note: this API proposal is not a replacement for #31619, but a compliment to it.

Open Questions

  • Will the API check for any conflicts in enum field naming? Or leave the result as of such state undefined?
  • Should we use JsonPropertyNameAttribute instead of the proposed new attribute?
    • I would recommend not to because the name JsonPropertyNameAttribute does not imply that it could be used on an enum.
    • I am not opposed to this approach, however.
  • Should we actually support the attributes in the System.Runtime.Serialization namespace?
    • I believe that this is a wider discussion than this narrow use case and should require a separate issue to discuss it as it would break the current affinity towards the serializer's own attributes.
  • Could we do without this proposal if System.Text.Json: JsonStringEnumConverter ignores its JsonNamingPolicy during deserialization. #31619 is implemented?
    • Yes, however, I believe there are some business cases where the name would need to be overridden even with a JsonNamingPolicy

Pull Request

See pull request #37113 for an example implementation.

Updates

@MattMinke
Copy link

I have a hunch alot of people right now are working around this issue by using @CodeBlanch recommendation. The nuget package referenced has 500,000 + downloads.

image

@CodeBlanch
Copy link
Contributor

CodeBlanch commented Apr 9, 2021

Ya a lot of people using it and some other features have been added based on issues/PRs opened in the repo behind it. It now supports using JsonPropertyName or EnumMemberAttribute (if you are targeting .NET 5+). Has a fix for #31619. Has a feature for defining a fallback value in case something unknown comes in during de-serialization. Also allows you to specify options based on the enum types being converted, or via an attribute, where the stock one you really only can apply settings globally (1 factory per serializer options).

I think the other popular one in the NuGet is the TimeSpan converter. Based on traffic to the blog post about it.

@JasonBodley
Copy link

Noddy work-around for those who need it

public class JsonStringEnumConverterEx<TEnum> : JsonConverter<TEnum> where TEnum : struct, System.Enum
  {

    private readonly Dictionary<TEnum, string> _enumToString = new Dictionary<TEnum, string>();
    private readonly Dictionary<string, TEnum> _stringToEnum = new Dictionary<string, TEnum>();

    public JsonStringEnumConverterEx()
    {
      var type = typeof(TEnum);
      var values = System.Enum.GetValues<TEnum>();

      foreach (var value in values)
      {
        var enumMember = type.GetMember(value.ToString())[0];
        var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
          .Cast<EnumMemberAttribute>()
          .FirstOrDefault();

        _stringToEnum.Add(value.ToString(), value);

        if (attr?.Value != null)
        {
          _enumToString.Add(value, attr.Value);
          _stringToEnum.Add(attr.Value, value);
        } else
        {
          _enumToString.Add(value, value.ToString());
        }
      }
    }

    public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
      var stringValue = reader.GetString();

      if (_stringToEnum.TryGetValue(stringValue, out var enumValue))
      {
        return enumValue;
      }

      return default;
    }

    public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
    {
      writer.WriteStringValue(_enumToString[value]);
    }
  }

...

[JsonConverter(typeof(JsonStringEnumConverterEx<MyEnum>))]
public MyEnum SomeValue { get; set; }

@egbertn
Copy link

egbertn commented Aug 27, 2021

Noddy work-around for those who need it

Thanks, I had to modify it a bit to get it working for a scenario where the deserialiser, receives numbers instead of the values.


public class JsonStringEnumConverterEx<TEnum> : JsonConverter<TEnum> where TEnum : struct, System.Enum
    {

        private readonly Dictionary<TEnum, string> _enumToString = new();
        private readonly Dictionary<string, TEnum> _stringToEnum = new();
        private readonly Dictionary<int, TEnum> _numberToEnum = new ();

        public JsonStringEnumConverterEx()
        {
            var type = typeof(TEnum);
           
            foreach (var value in Enum.GetValues<TEnum>())
            {
                var enumMember = type.GetMember(value.ToString())[0];
                var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
                  .Cast<EnumMemberAttribute>()
                  .FirstOrDefault();

                _stringToEnum.Add(value.ToString(), value);
                var num =Convert.ToInt32( type.GetField("value__")
                        .GetValue(value));
                if (attr?.Value != null)
                {
                    _enumToString.Add(value, attr.Value);
                    _stringToEnum.Add(attr.Value, value);
                    _numberToEnum.Add(num, value);
                }
                else
                {
                    _enumToString.Add(value, value.ToString());
                }
            }
        }

        public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var type = reader.TokenType;
            if (type == JsonTokenType.String)
            {
                var stringValue = reader.GetString();
                if (_stringToEnum.TryGetValue(stringValue, out var enumValue))
                {
                    return enumValue;
                }
            }
            else  if (type == JsonTokenType.Number)
            {
                var numValue = reader.GetInt32();
                _numberToEnum.TryGetValue(numValue, out var enumValue);
                return enumValue;
            }

            return default;
        }

        public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(_enumToString[value]);
        }
    }

@eiriktsarpalis
Copy link
Member

I'm going to close this in favor of #29975.

@mdddev
Copy link

mdddev commented Nov 4, 2021

Noddy work-around for those who need it

Thanks, I had to modify it a bit to get it working for a scenario where the deserialiser, receives numbers instead of the values.


public class JsonStringEnumConverterEx<TEnum> : JsonConverter<TEnum> where TEnum : struct, System.Enum
    {

        private readonly Dictionary<TEnum, string> _enumToString = new();
        private readonly Dictionary<string, TEnum> _stringToEnum = new();
        private readonly Dictionary<int, TEnum> _numberToEnum = new ();

        public JsonStringEnumConverterEx()
        {
            var type = typeof(TEnum);
           
            foreach (var value in Enum.GetValues<TEnum>())
            {
                var enumMember = type.GetMember(value.ToString())[0];
                var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
                  .Cast<EnumMemberAttribute>()
                  .FirstOrDefault();

                _stringToEnum.Add(value.ToString(), value);
                var num =Convert.ToInt32( type.GetField("value__")
                        .GetValue(value));
                if (attr?.Value != null)
                {
                    _enumToString.Add(value, attr.Value);
                    _stringToEnum.Add(attr.Value, value);
                    _numberToEnum.Add(num, value);
                }
                else
                {
                    _enumToString.Add(value, value.ToString());
                }
            }
        }

        public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var type = reader.TokenType;
            if (type == JsonTokenType.String)
            {
                var stringValue = reader.GetString();
                if (_stringToEnum.TryGetValue(stringValue, out var enumValue))
                {
                    return enumValue;
                }
            }
            else  if (type == JsonTokenType.Number)
            {
                var numValue = reader.GetInt32();
                _numberToEnum.TryGetValue(numValue, out var enumValue);
                return enumValue;
            }

            return default;
        }

        public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(_enumToString[value]);
        }
    }

Hi, thanks for this!

"{\"Name\": \"EnumTest\", \"Value\": 123}" De-serializing works if a number is received. However it also serializes to "123" instead of 123. Can you recommend a modification?

@ghost ghost locked as resolved and limited conversation to collaborators Dec 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json enhancement Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

No branches or pull requests