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 customizing enum member names in System.Text.Json #74385

Closed
jscarle opened this issue Aug 22, 2022 · 50 comments · Fixed by #105032
Closed

Support customizing enum member names in System.Text.Json #74385

jscarle opened this issue Aug 22, 2022 · 50 comments · Fixed by #105032
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json in-pr There is an active PR which will close this issue when it is merged
Milestone

Comments

@jscarle
Copy link

jscarle commented Aug 22, 2022

API Proposal

Concerning 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.Field, AllowMultiple = false)]
public class JsonStringEnumMemberNameAttribute : Attribute
{
    public JsonStringEnumMemberNameAttribute(string name);
    public string Name { get; }
}

API Usage

Setting 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,
}
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 and DataMember attributes in general. Although JsonStringEnumConverter 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:

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GroupType
{
    [EnumMember(Value = "A")]
    Administrator,

    [EnumMember(Value = "U")]
    User
}

Suggested Workaround

See this gist for a recommended workaround that works for both AOT and reflection-based scenaria.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Aug 22, 2022
@ghost
Copy link

ghost commented Aug 22, 2022

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

Issue Details

Issue 31081 was closed in favor of issue 29975, however the scope of the issues differ.

Issues 29975 is a discussion regarding the DataContract and DataMember attributes in general. Although JsonStringEnumConverter 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:

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GroupType
{
    [EnumMember(Value = "A")]
    Administrator,

    [EnumMember(Value = "U")]
    User
}

The solution proposed by JasonBodley works correctly: #31081 (comment)

[JsonConverter(typeof(JsonStringEnumConverterEx<GroupType>))]
public enum GroupType
{
    [EnumMember(Value = "A")]
    Administrator,

    [EnumMember(Value = "U")]
    User
}

However, this functionality should be built-in to the JsonStringEnumConverter.

Author: jscarle
Assignees: -
Labels:

area-System.Text.Json, untriaged

Milestone: -

@eiriktsarpalis
Copy link
Member

As mentioned in the issues you're linking to, it is unlikely we would add support for EnumMemberAttribute specifically, for the same reason that we are reluctant to do it for all the other attributes under System.Runtime.Serialization. These pertain to different/older serialization stacks and as such supporting them OOTB in System.Text.Json is not something we are planning to do.

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.

@eiriktsarpalis eiriktsarpalis removed the untriaged New issue has not been triaged by the area owner label Aug 23, 2022
@eiriktsarpalis eiriktsarpalis added this to the Future milestone Aug 23, 2022
@eiriktsarpalis eiriktsarpalis changed the title Support for EnumMemberAttribute in JsonStringEnumConverter Support customizing enum member names in System.Text.Json Aug 23, 2022
@eiriktsarpalis eiriktsarpalis added the wishlist Issue we would like to prioritize, but we can't commit we will get to it yet label Aug 23, 2022
@jscarle
Copy link
Author

jscarle commented Aug 23, 2022

Looking at the System.Text.Json.Serialization namespace, in addition to the JsonConverter and JsonConverter<T>, the only other JsonConverter added was the JsonStringEnumConverter. In other words, it was clearly identified that there is a frequent use case for converting between json string values and their enum equivalents, and vice-versa. However, right from the very beginning, people ran into situations where the string value is not necessarily the exact name of the enum, as it has been clearly demonstrated on stackoverflow: https://stackoverflow.com/questions/59059989/system-text-json-how-do-i-specify-a-custom-name-for-an-enum-value

I do agree that EnumMember is not the correct attribute, and that moving forward the goal should be to build upon System.Text.Json and not another unrelated namespace.

In the spirit of what's already been established with JsonStringEnumConverter, I would propose the following attribute:

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 JsonStringEnumConverter specifically. Extending support for this attribute would eliminate the need for workarounds and nuget packages that people currently use and allow JsonStringEnumConverter to be used as the built-in go-to for all string to enum use cases.

@eiriktsarpalis
Copy link
Member

Extending support for this attribute would eliminate the need for workarounds and nuget packages that people currently use

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.

@jscarle
Copy link
Author

jscarle commented Aug 23, 2022

I do agree, and there are popular alternatives such as Newtonsoft.Json which can fulfill the need for more advanced JSON serialization and deserialization. Its simply that the functionality for JsonStringEnumConverter has already been included in System.Text.Json, it would be nice to round off that functionality with this last missing bit.

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 Enum.TryParse (which is eventually called by JsonStringEnumConverter) to compensate for spaces, dashes, and underscores, but being that it's such a core method in the runtime, I doubt that would ever see the light of day.

@IanKemp
Copy link

IanKemp commented Oct 12, 2022

Extending support for this attribute would eliminate the need for workarounds and nuget packages that people currently use

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.

Do "the right extensibility points" include "hiding JsonStringEnumConverter's functionality in EnumConverter<T>, an internal sealed class that precludes said functionality from being easily extended by developers, thus forcing them to re-implement that same functionality entirely from scratch"?

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?

@layomia
Copy link
Contributor

layomia commented Nov 2, 2022

The contract resolver feature new in 7.0 could be extended to support user detection of EnumMemberAttribute with minimal configuration code to apply it to enum contracts. We can evaluate the feasibility of enabling this scenario in 8.0.

@Maximys
Copy link
Contributor

Maximys commented Dec 1, 2022

@eiriktsarpalis , I want to provide some links about current Issue:

  1. System.Text.Json.Tests.EnumConverterTests.DuplicateNameEnumTest;
  2. System.Text.Json.Tests.EnumConverterTests.DuplicateNameEnum.FooBar.

Yes, may be I does not understand something, but this test and enum assume the implementation of EnumMemberAttribute support.
Today I had alot of hadacke with JsonSerializer and EnumMemberAttribute. Can I try to implement this support?

@bunnyi116
Copy link

bunnyi116 commented Mar 6, 2023

Original content

我认为 JsonStringEnumConverter 应该内置一个自定义名称功能,因为 JsonConverter 使用枚举属性的字符串。

使用 JsonStringEnumConverter 时,应该检查 JsonPropertyNameAttribute 属性,如果有,则使用属性JsonPropertyName中的名称

对于 Enum 属性名称特性命名,可以直接使用 JsonPropertyNameAttribute 统一属性名称代码样式,而不应使用 EnumMemberAttribute

注:由于我的英语不好,我使用了机器翻译。如果有翻译错误,请原谅我。

Machine translation

I think JsonStringEnumConverter should have a custom name feature built in, because JsonConverter uses strings for enum properties.

When using JsonStringEnumConverter, you should check the JsonPropertyNameAttribute attribute and use the name from the attribute JsonPropertyName if present

For Enum property name attribute naming, you can directly use JsonPropertyNameAttribute to unify the property name code style instead of EnumMemberAttribute

Note: Since my English is not good, I used machine translation. Forgive me if there are translation errors.

@bunnyi116
Copy link

bunnyi116 commented Mar 6, 2023

The contract resolver feature new in 7.0 could be extended to support user detection of EnumMemberAttribute with minimal configuration code to apply it to enum contracts. We can evaluate the feasibility of enabling this scenario in 8.0.

Original content

我希望Attribute是这个 JsonPropertyNameAttribute,而不是EnumMemberAttribute。

因为我觉得JsonPropertyNameAttribute代表的是Json属性名称,所以有关Json序列化和反序列化的属性名称应该使用JsonPropertyNameAttribute。

注:由于我的英语不好,我使用了机器翻译。如果有翻译错误,请原谅我。

Machine translation

I 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.

@bunnyi116
Copy link

bunnyi116 commented Mar 6, 2023

Interim programme

Temporarily available methods

Converter

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

Demo

var 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

Test { TestEnum0 = Enum0, TestEnum1 = Enum1, TestEnum2 = Enum2, TestEnum3 = Enum3 }
———————————————————————————————————————————————————————————————————
{"TestEnum0":"name0","TestEnum1":"name1","TestEnum2":2,"TestEnum3":3}
Test { TestEnum0 = Enum0, TestEnum1 = Enum1, TestEnum2 = Enum2, TestEnum3 = Enum3 }
———————————————————————————————————————————————————————————————————
Test { TestEnum0 = Enum1, TestEnum1 = Enum0, TestEnum2 = Enum3, TestEnum3 = Enum2 }

@bunnyi116
Copy link

bunnyi116 commented Mar 6, 2023

Looking at the System.Text.Json.Serialization namespace, in addition to the JsonConverter and JsonConverter<T>, the only other JsonConverter added was the JsonStringEnumConverter. In other words, it was clearly identified that there is a frequent use case for converting between json string values and their enum equivalents, and vice-versa. However, right from the very beginning, people ran into situations where the string value is not necessarily the exact name of the enum, as it has been clearly demonstrated on stackoverflow: https://stackoverflow.com/questions/59059989/system-text-json-how-do-i-specify-a-custom-name-for-an-enum-value

I do agree that EnumMember is not the correct attribute, and that moving forward the goal should be to build upon System.Text.Json and not another unrelated namespace.

In the spirit of what's already been established with JsonStringEnumConverter, I would propose the following attribute:

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 JsonStringEnumConverter specifically. Extending support for this attribute would eliminate the need for workarounds and nuget packages that people currently use and allow JsonStringEnumConverter to be used as the built-in go-to for all string to enum use cases.

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?

@Corbie-42
Copy link

Corbie-42 commented May 23, 2023

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 JsonEnumNameAttribute, to that it is equal to the JsonPropertyNameAttribute. The value of the enum should not be serialized.

@eduherminio
Copy link
Member

Am I right to understand that there's no built-in way to use System.Text.Json to deserialize an enum that comes as a string with dashes (i.e. "enum-1", "enum-2" values) to a C# enum?

@eiriktsarpalis eiriktsarpalis modified the milestones: Future, 9.0.0 Jul 16, 2023
@KSemenenko
Copy link

this is usful, can't wait for it

@eiriktsarpalis
Copy link
Member

I either write my own converters (tedious) or use the third party Macross.Json.Extensions NuGet package (not AOT compatible).

Have you considered the workaround proposed here? #74385 (comment)

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Jul 10, 2024

To set expectations straight, System.Text.Json won't be supporting EnumMemberAttribute out of the box because the STJ assembly doesn't have a dependency on System.Runtime.Serialization. For those wanting to add EnumMemberAttribute support you can find suggested workarounds here and here.

API Proposal

Concerning 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 Usage

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

@rcollette
Copy link

@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.

@julealgon
Copy link

Have you considered perhaps an even more general version of the [DataMember] attributes, something that would sit on the DataAnnotations namespace along with stuff like [Display], or maybe in an even lower System level namespace/assembly, that would serve to indicate a more "raw" name override for the property/element/etc?

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, nameof(MyMethod) would return "SomethingElseAltogether", so this would be honored at compile-time as well.

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?

@MSACATS
Copy link

MSACATS commented Jul 10, 2024

Have you considered perhaps an even more general version of the [DataMember] attributes, something that would sit on the DataAnnotations namespace along with stuff like [Display], or maybe in an even lower System level namespace/assembly, that would serve to indicate a more "raw" name override for the property/element/etc?

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, nameof(MyMethod) would return "SomethingElseAltogether", so this would be honored at compile-time as well.

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

@jscarle
Copy link
Author

jscarle commented Jul 10, 2024

API Proposal

Concerning 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 Usage

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

That's perfect! 👍🏻

@eiriktsarpalis eiriktsarpalis self-assigned this Jul 10, 2024
@eiriktsarpalis eiriktsarpalis added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed wishlist Issue we would like to prioritize, but we can't commit we will get to it yet labels Jul 10, 2024
@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Jul 13, 2024

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 DynamicallyAccessedMembers declarations across a number of public APIs.

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 EnumMemberAttribute.

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 Questions

The design needs to account for JsonStringEnumConverter annotations made on individual properties. The above only works for type-level annotations.

@eiriktsarpalis eiriktsarpalis added api-suggestion Early API idea and discussion, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 13, 2024
@eiriktsarpalis
Copy link
Member

I've created a gist that combines both workarounds into one source file.

@HavenDV
Copy link

HavenDV commented Jul 13, 2024

I'm creating an SDKs generator based on OpenAPI, and I also recently solved an issue with System.Text.Json and enums.
I have a question - now for each enum I generate fast conversion extensions like these (https://github.com/andrewlock/NetEscapades.EnumGenerators), and for each enum I create my own converter that uses these extensions. This is fully compatible with Trimming/NativeAOT and logically should also be performant, but I am confused by hundreds and thousands of converters for complex APIs, can their presence negate the advantages?

@bunnyi116
Copy link

I think I think JsonPropertyName can be NativeAOT, and I think it can also achieve this function.

@dotnet-policy-service dotnet-policy-service bot added in-pr There is an active PR which will close this issue when it is merged labels Jul 17, 2024
@eiriktsarpalis
Copy link
Member

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.

@eiriktsarpalis eiriktsarpalis added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jul 17, 2024
@eiriktsarpalis
Copy link
Member

API as proposed in #74385 (comment) has been approved over email. cc @stephentoub @terrajobst

@eiriktsarpalis eiriktsarpalis added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 22, 2024
@jscarle
Copy link
Author

jscarle commented Jul 22, 2024

@eiriktsarpalis Thank you for completing this! I have a couple of questions?

  1. Will this be part of .NET 9?

  2. Is deserializing case insensitive?

  3. Does deserializing now throw an exception on invalid values?

@eiriktsarpalis
Copy link
Member

  1. Yes, it should be available in Preview 7.
  2. No. This is similar to how JsonNamingPolicy is being handled by the converter.
  3. Yes, if you are asking about invalid string identifiers. Numeric values would still be picked up if the converter has been configured accordingly.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json in-pr There is an active PR which will close this issue when it is merged
Projects
None yet
Development

Successfully merging a pull request may close this issue.