Closed kolkinn closed 3 years ago
/cc @steveharter
We should discuss if we want to support this and other attributes from System.Runtime.Serialization
before adding them.
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.
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.
From @AraHaan in https://github.com/dotnet/runtime/issues/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?"
Here is the new pull request for this issue: #37113.
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.
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.
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
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.
JsonPropertyNameAttribute
instead of the proposed new attribute?
JsonPropertyNameAttribute
does not imply that it could be used on an enum.System.Runtime.Serialization
namespace?
JsonNamingPolicy
See pull request #37113 for an example implementation.
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.
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.
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; }
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
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]);
}
}
I'm going to close this in favor of #29975.
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?
Hi, I wonder if supporting EnumMemberAttribute in JsonConverterEnum is planned and if so what the ETA is?