-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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 customizing enum member names in System.Text.Json #74385
Comments
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis Issue DetailsIssue 31081 was closed in favor of issue 29975, however the scope of the issues differ. Issues 29975 is a discussion regarding the The following enum will NOT convert properly using the current implementation of
The solution proposed by JasonBodley works correctly: #31081 (comment)
However, this functionality should be built-in to the
|
As mentioned in the issues you're linking to, it is unlikely we would add support for We might consider exposing a dedicated attribute in the future, assuming there is substantial demand. Alternatively, it should be possible to add support using a custom converter, as has already been proposed by the community. |
Looking at the I do agree that In the spirit of what's already been established with namespace System.Text.Json.Serialization
{
/// <summary>
/// Specifies the enum string value that is present in the JSON when serializing and deserializing.
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class JsonStringEnumValueAttribute : JsonAttribute
{
/// <summary>
/// Initializes a new instance of <see cref="JsonStringEnumValueAttribute"/> with the specified string value.
/// </summary>
/// <param name="value">The string value of the enum.</param>
public JsonStringEnumValueAttribute(string value)
{
Value = value;
}
/// <summary>
/// The string value of the enum.
/// </summary>
public string Value { get; }
}
} Which could then be used by the |
I should clarify that implementing all functionality OOTB is not a design goal for System.Text.Json. We do want to make sure the library offers the right extensibility points so that third party extensions can be defined easily and be successfully, however. |
I do agree, and there are popular alternatives such as String to enum conversions is a frequent use case, and due to the language restrictions for enum members, the occurrences where the string representation of the enum value will differ from the enum member name is quite frequent. Quite often due to the use of spaces, dashes, or underscores in the JSON string value used by the system with which .NET code may be communicating. An alternative would be to enhance the parsing logic of |
Do "the right extensibility points" include "hiding Why is it that every time I need to do something simple like this feature request in STJ, I end up spending half a day looking for a solution, which turns out to be the opposite of simple... and along the way I find a similar solution for the same requirement in Newtonsoft.Json, and it is absolutely simple there? I very much get the impression that the STJ APIs and classes were designed by people who have never had to try to use STJ, whereas Newtonsoft.Json was written by a developer for developers. To put it simply, Microsoft: if you want people to use STJ, why do you continually make it so difficult to use STJ? Why do you make me regret my decision to use it every time I try to use it? Why does this API have to be so unnecessarily, continually painful? |
The contract resolver feature new in 7.0 could be extended to support user detection of |
@eiriktsarpalis , I want to provide some links about current Issue:
Yes, may be I does not understand something, but this test and enum assume the implementation of EnumMemberAttribute support. |
Original content我认为 使用 对于 注:由于我的英语不好,我使用了机器翻译。如果有翻译错误,请原谅我。 Machine translationI think When using For Note: Since my English is not good, I used machine translation. Forgive me if there are translation errors. |
Original content我希望Attribute是这个 JsonPropertyNameAttribute,而不是EnumMemberAttribute。 因为我觉得JsonPropertyNameAttribute代表的是Json属性名称,所以有关Json序列化和反序列化的属性名称应该使用JsonPropertyNameAttribute。 注:由于我的英语不好,我使用了机器翻译。如果有翻译错误,请原谅我。 Machine translationI want the Attribute to be this JsonPropertyNameAttribute instead of EnumMemberAttribute. Because I think JsonPropertyNameAttribute represents the Json property name, so the Property name for Json serialization and deserialization should use JsonPropertyNameAttribute. Note: Since my English is not good, I used machine translation. Forgive me if there are translation errors. |
Interim programmeTemporarily available methods Converterpublic class JsonStringEnumConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum;
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var type = typeof(JsonStringEnumConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(type)!;
}
}
public class JsonStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
{
private readonly Dictionary<TEnum, string> _enumToString = new();
private readonly Dictionary<string, TEnum> _stringToEnum = new();
private readonly Dictionary<int, TEnum> _numberToEnum = new();
public JsonStringEnumConverter()
{
var type = typeof(TEnum);
foreach (var value in Enum.GetValues<TEnum>())
{
var enumMember = type.GetMember(value.ToString())[0];
var attr = enumMember.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false)
.Cast<JsonPropertyNameAttribute>()
.FirstOrDefault();
var num = Convert.ToInt32(type.GetField("value__")?.GetValue(value));
if (attr?.Name != null)
{
_enumToString.Add(value, attr.Name);
_stringToEnum.Add(attr.Name, value);
_numberToEnum.Add(num, value);
}
else
{
_enumToString.Add(value, value.ToString());
_stringToEnum.Add(value.ToString(), value);
_numberToEnum.Add(num, value);
}
}
}
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var type = reader.TokenType;
if (type == JsonTokenType.String)
{
var stringValue = reader.GetString();
if (stringValue != null && _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]);
}
} Demovar test = new Test()
{
TestEnum0 = TestEnum.Enum0,
TestEnum1 = TestEnum.Enum1,
TestEnum2 = TestEnum.Enum2,
TestEnum3 = TestEnum.Enum3,
};
Console.WriteLine(test);
Console.WriteLine("———————————————————————————————————————————————————————————————————");
var json = JsonSerializer.Serialize(test);
var obj = JsonSerializer.Deserialize<Test>(json);
Console.WriteLine(json);
Console.WriteLine(obj);
Console.WriteLine("———————————————————————————————————————————————————————————————————");
var str = """
{
"TestEnum0":"name1",
"TestEnum1":0,
"TestEnum2":3,
"TestEnum3":2
}
""";
obj = JsonSerializer.Deserialize<Test>(str);
Console.WriteLine(obj);
record Test
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public required TestEnum TestEnum0 { get; init; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public required TestEnum TestEnum1 { get; init; }
public required TestEnum TestEnum2 { get; init; }
public required TestEnum TestEnum3 { get; init; }
}
enum TestEnum
{
[JsonPropertyName("name0")]
Enum0,
[JsonPropertyName("name1")]
Enum1,
Enum2,
Enum3,
} Result
|
I think it's right, a new name starting with Json should be used. According to the naming of JsonPropertyName, I think it might be more appropriate to name it JsonEnumNameAttribute or JsonEnumValueAttribute? |
I vote for |
Am I right to understand that there's no built-in way to use |
this is usful, can't wait for it |
Have you considered the workaround proposed here? #74385 (comment) |
To set expectations straight, System.Text.Json won't be supporting API ProposalConcerning the question of adding built-in enum name customization support, this would need to be done via a new attribute type: namespace System.Text.Json.Serialization;
[AttributeUsage(AttributeTargets.Enum, AllowMultiple = false)]
public class JsonStringEnumMemberNameAttribute : Attribute
{
public JsonStringEnumMemberNameAttribute(string name);
public string Name { get; }
} API UsageSetting the attribute on individual enum members can customize their name JsonSerializer.Serialize(MyEnum.Value1 | MyEnum.Value2); // "A, B"
[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
public enum MyEnum
{
[JsonStringEnumMemberName("A")]
Value1 = 1,
[JsonStringEnumMemberName("B")]
Value2 = 2,
} |
@eiriktsarpalis That API Proposal seems appropriate. Perhaps it is implied that this will include alignment with API discovery such that string enumeration values are included (ex. for OpenAPI spec generation) but it wouldn't hurt to call that out as well. |
Have you considered perhaps an even more general version of the I feel like every library keeps redefining these attributes all the time with the same underlying purpose, and that we should instead come up with a more generic "renaming" mechanism that works more seamlessly with all libraries. For example, what if there was a lower level attribute that actually changed the results of reflection, so that consumers would transparently honor the renamed fields/members even without actively searching for a specific attribute? Such attribute could be used for basically every member type... including even methods. Then, when calls are made to either match or get those elements, the name override would be used/returned instead of the name defined in the identifier. Example: public class MyClass
{
[MemberName("SomethingElseAltogether")]
public void MyMethod()...
} Then the calls would work like this: var myMethodOriginal = typeof(MyClass).GetMethod("MyMethod"); // returns `null`. no match
var myMethodOverriden = typeof(MyClass).GetMethod("SomethingElseAltogether"); // returns the member info, finds the match Similarly, This lower-level override would then propagate to every single consumer be it a source generator, a reflection-based scan or anything else, and honor the new names. Libraries would be simpler (as they don't have to check for attributes, or create custom ones) and the behavior would be finally unified. Thoughts? |
There is (was) a rich ecosystem for this sort of thing as it pertains to reflection (TypeDescriptor): see e.g. https://putridparrot.com/blog/dynamically-extending-an-objects-properties-using-typedescriptor/ I don't think the very low-level things (nameof/etc) are appropriate to change in this way -- if the name needs to be changed at such a low level, the motivation eludes me as to why you wouldn't change the actual name |
That's perfect! 👍🏻 |
I spent a bit of time prototyping an implementation, but it turns out that the current proposed design stumbles on the source generator. TL;DR it would require resolving enum member names using reflection which in turn forces viral As it stands the proposed API isn't fit for purpose -- it would additionally require a number of extensions on the contract APIs such that custom enum metadata can be mapped at compile time by the source generator. This is nontrivial work, so it's possible that it won't make .NET 9 (for which feature development is set to conclude in the coming weeks). In the meantime I invite you to apply the workarounds as proposed here and here -- they provide a fully functional substitute that works with the existing API Proposal (Updated)namespace System.Text.Json.Serialization;
+[AttributeUsage(AttributeTargets.Enum, AllowMultiple = false)]
+public class JsonStringEnumMemberNameAttribute : Attribute
+{
+ public JsonStringEnumMemberNameAttribute(string name);
+ public string Name { get; }
+}
namespace System.Text.Json.Serialization.Metadata;
public enum JsonTypeInfoKind
{
None,
Object,
Enumerable,
Dictionary,
+ Enum // Likely breaking change (kind for enums currently reported as 'None')
}
+[Flags]
+public enum JsonEnumConverterFlags
+{
+ None = 0,
+ AllowNumbers = 1,
+ AllowStrings = 2,
+}
public partial class JsonTypeInfo
{
public JsonTypeInfoKind Kind { get; }
+ public JsonEnumConverterFlags EnumConverterFlags { get; set; }
+ // The source generator will incorporate JsonStringEnumMemberNameAttribute support by implementing its own naming policy
+ public JsonNamingPolicy? EnumNamingPolicy { get; set; }
} Open QuestionsThe design needs to account for |
I've created a gist that combines both workarounds into one source file. |
I'm creating an SDKs generator based on OpenAPI, and I also recently solved an issue with System.Text.Json and enums. |
I think I think JsonPropertyName can be NativeAOT, and I think it can also achieve this function. |
After further experimenation, I was able to produce an implementation that makes the attribute work in AOT without added APIs. Now to API review this. |
API as proposed in #74385 (comment) has been approved over email. cc @stephentoub @terrajobst |
@eiriktsarpalis Thank you for completing this! I have a couple of questions?
|
|
API Proposal
Concerning the question of adding built-in enum name customization support, this would need to be done via a new attribute type:
API Usage
Setting the attribute on individual enum members can customize their name
Original Post
[Issue 31081](https://github.com//issues/31081) was closed in favor of [issue 29975](https://github.com//issues/29975), however the scope of the issues differ.Issues 29975 is a discussion regarding the
DataContract
andDataMember
attributes in general. AlthoughJsonStringEnumConverter
does address the straight conversion between an enum and its direct string representation, it does not in fact address cases where the string is not a direct match to the enum value.The following enum will NOT convert properly using the current implementation of
JsonStringEnumConverter
:Suggested Workaround
See this gist for a recommended workaround that works for both AOT and reflection-based scenaria.
The text was updated successfully, but these errors were encountered: