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

2

75

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
}
Myrticemyrtie answered 26/11, 2019 at 22:0 Comment(3)
Not supported via attributes in .net-core-3.0, see Support for EnumMemberAttribute in JsonConverterEnum #41578.Crestfallen
Probably with a JsonStringEnumConverterPablo
Do you need to use System.Text.Json? Or would it suit you to switch to Newtonsoft.Json?Dilisio
C
106

This is not currently supported out of the box in , , , or .

Microsoft, in their infinite wisdom, have elected to close the relevant issue requesting this functionality, essentially because they can't be bothered to implement it. Thus you will need to create your own JsonConverterFactory that serializes enums with custom value names specified by attributes, or use a NuGet package that does the same such as Macross.Json.Extensions.

If you are working in .NET 7 or later, or only need to serialize but not deserialize enums with custom names in earlier versions, custom names can be easily supported by creating a JsonConverterFactory that adapts JsonStringEnumConverter by constructing a customized JsonNamingPolicy for each enum with [EnumMember(Value = "xxx")] applied to any enum value.

I chose EnumMemberAttribute because this is the attribute supported by Newtonsoft, but you could use JsonPropertyNameAttribute instead if you prefer.

First, introduce the following converter:

public class JsonEnumMemberStringEnumConverter : JsonConverterFactory
{
    private readonly JsonNamingPolicy? namingPolicy;
    private readonly bool allowIntegerValues;
    private readonly JsonStringEnumConverter baseConverter;

    public JsonEnumMemberStringEnumConverter() : this(null, true) { }

    public JsonEnumMemberStringEnumConverter(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 && attr.Value != 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?.ConvertName(name) ?? 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 JsonEnumMemberStringEnumConverter() },
    // Set other options as required:
    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:

  • In .NET 6 and earlier, JsonStringEnumConverter ignores its naming policy during deserialization, see System.Text.Json: JsonStringEnumConverter ignores its JsonNamingPolicy during deserialization #31619 for details. This was fixed with pull 73348.

  • 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 is "Trick, TrickOrTreat".

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

Demo .NET 7 fiddle here.

If you need to round-trip an enum with custom value names in .NET 6 or earlier 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 converting enums of all possible underlying types (byte, short, int, long, ulong etc).

JsonStringEnumMemberConverter from the aforementioned 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:

// This third-party converter was placed in a system namespace.
[JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumMemberConverter))]
public enum Example 
{
    Trick,
    Treat,
    [EnumMember(Value = "Trick-Or-Treat")]
    TrickOrTreat,
}

See the docs here for usage details.

Alternatively you could roll your own. One possibility is shown below. It is written against .NET 6 and would need some backporting to earlier versions:

public class JsonPropertyNameStringEnumConverter : GeneralJsonStringEnumConverter
{
    public JsonPropertyNameStringEnumConverter() : base() { }
    public JsonPropertyNameStringEnumConverter(JsonNamingPolicy? namingPolicy = default, bool allowIntegerValues = true) : base(namingPolicy, allowIntegerValues) { }

    protected override bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
    {
        if (JsonEnumExtensions.TryGetEnumAttribute<JsonPropertyNameAttribute>(enumType, name, out var attr) && attr.Name != null)
        {
            overrideName = attr.Name.AsMemory();
            return true;
        }
        return base.TryOverrideName(enumType, name, out overrideName);
    }
}

public class JsonEnumMemberStringEnumConverter : GeneralJsonStringEnumConverter
{
    public JsonEnumMemberStringEnumConverter() : base() { }
    public JsonEnumMemberStringEnumConverter(JsonNamingPolicy? namingPolicy = default, bool allowIntegerValues = true) : base(namingPolicy, allowIntegerValues) { }

    protected override bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
    {
        if (JsonEnumExtensions.TryGetEnumAttribute<System.Runtime.Serialization.EnumMemberAttribute>(enumType, name, out var attr) && attr.Value != null)
        {
            overrideName = attr.Value.AsMemory();
            return true;
        }
        return base.TryOverrideName(enumType, name, out overrideName);
    }
}

public delegate bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName);

public class GeneralJsonStringEnumConverter : JsonConverterFactory
{
    readonly JsonNamingPolicy? namingPolicy;
    readonly bool allowIntegerValues;
    
    public GeneralJsonStringEnumConverter() : this(null, true) { }
    
    public GeneralJsonStringEnumConverter(JsonNamingPolicy? namingPolicy = default, bool allowIntegerValues = true) => (this.namingPolicy, this.allowIntegerValues) = (namingPolicy, allowIntegerValues);

    public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;

    public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
        var flagged = enumType.IsDefined(typeof(FlagsAttribute), true);
        JsonConverter enumConverter;
        TryOverrideName tryOverrideName = (Type t, string n, out ReadOnlyMemory<char> o) => TryOverrideName(t, n, out o);
        var converterType = (flagged ? typeof(FlaggedJsonEnumConverter<>) : typeof(UnflaggedJsonEnumConverter<>)).MakeGenericType(new [] {enumType});
        enumConverter = (JsonConverter)Activator.CreateInstance(converterType,
                                                                BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
                                                                binder: null,
                                                                args: new object[] { namingPolicy!, allowIntegerValues, tryOverrideName },
                                                                culture: null)!;
        if (enumType == typeToConvert)
            return enumConverter;
        else
        {
            var nullableConverter = (JsonConverter)Activator.CreateInstance(typeof(NullableConverterDecorator<>).MakeGenericType(new [] {enumType}), 
                                                                            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
                                                                            binder: null,
                                                                            args: new object[] { enumConverter },
                                                                            culture: null)!;
            return nullableConverter;
        }
    }
    
    protected virtual bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
    {
        overrideName = default;
        return false;
    }
    
    class FlaggedJsonEnumConverter<TEnum> : JsonEnumConverterBase<TEnum> where TEnum: struct, Enum
    {
        private const char FlagSeparatorChar = ',';
        private const string FlagSeparatorString = ", ";

        public FlaggedJsonEnumConverter(JsonNamingPolicy? namingPolicy, bool allowNumbers, TryOverrideName? tryOverrideName) : base(namingPolicy, allowNumbers, tryOverrideName) { }

        protected override bool TryFormatAsString(EnumData<TEnum> [] enumData, TEnum value, out ReadOnlyMemory<char> name)
        {
            UInt64 UInt64Value = JsonEnumExtensions.ToUInt64(value, EnumTypeCode);
            var index = enumData.BinarySearchFirst(UInt64Value, EntryComparer);
            if (index >= 0)
            {
                // A single flag
                name = enumData[index].name;
                return true;
            }
            if (UInt64Value != 0)
            {
                StringBuilder? sb = null;
                for (int i = (~index) - 1; i >= 0; i--)
                {
                    if ((UInt64Value & enumData[i].UInt64Value) ==  enumData[i].UInt64Value && enumData[i].UInt64Value != 0)
                    {
                        if (sb == null)
                        {
                            sb = new StringBuilder();
                            sb.Append(enumData[i].name.Span);
                        }
                        else
                        {
                            sb.Insert(0, FlagSeparatorString);
                            sb.Insert(0, enumData[i].name.Span);
                        }
                        UInt64Value -= enumData[i].UInt64Value;
                    }
                }
                if (UInt64Value == 0 && sb != null)
                {
                    name = sb.ToString().AsMemory();
                    return true;
                }
            }
            name = default;
            return false;
        }

        protected override bool TryReadAsString(EnumData<TEnum> [] enumData, ILookup<ReadOnlyMemory<char>, int> nameLookup, ReadOnlyMemory<char> name, out TEnum value)
        {
            UInt64 UInt64Value = 0;
            foreach (var slice in name.Split(FlagSeparatorChar, StringSplitOptions.TrimEntries))
            {
                if (JsonEnumExtensions.TryLookupBest<TEnum>(enumData, nameLookup, slice, out TEnum thisValue))
                    UInt64Value |= thisValue.ToUInt64(EnumTypeCode);
                else
                {
                    value = default;
                    return false;
                }
            }
            value = JsonEnumExtensions.FromUInt64<TEnum>(UInt64Value);
            return true;
        }
    }

    class UnflaggedJsonEnumConverter<TEnum> : JsonEnumConverterBase<TEnum> where TEnum: struct, Enum
    {
        public UnflaggedJsonEnumConverter(JsonNamingPolicy? namingPolicy, bool allowNumbers, TryOverrideName? tryOverrideName) : base(namingPolicy, allowNumbers, tryOverrideName) { }

        protected override bool TryFormatAsString(EnumData<TEnum> [] enumData, TEnum value, out ReadOnlyMemory<char> name)
        {
            var index = enumData.BinarySearchFirst(JsonEnumExtensions.ToUInt64(value, EnumTypeCode), EntryComparer);
            if (index >= 0)
            {
                name = enumData[index].name;
                return true;
            }
            name = default;
            return false;
        }
        protected override bool TryReadAsString(EnumData<TEnum> [] enumData, ILookup<ReadOnlyMemory<char>, int> nameLookup, ReadOnlyMemory<char> name, out TEnum value) => 
            JsonEnumExtensions.TryLookupBest(enumData, nameLookup, name, out value);
    }

    abstract class JsonEnumConverterBase<TEnum> : JsonConverter<TEnum> where TEnum: struct, Enum
    {
        protected static TypeCode EnumTypeCode { get; } = Type.GetTypeCode(typeof(TEnum));  
        protected static Func<EnumData<TEnum>, UInt64, int> EntryComparer { get; } = (item, key) => item.UInt64Value.CompareTo(key);

        private bool AllowNumbers { get; }
        private EnumData<TEnum> [] EnumData { get; }
        private ILookup<ReadOnlyMemory<char>, int> NameLookup { get; }

        public JsonEnumConverterBase(JsonNamingPolicy? namingPolicy, bool allowNumbers, TryOverrideName? tryOverrideName) 
        {
            this.AllowNumbers = allowNumbers;
            this.EnumData = JsonEnumExtensions.GetData<TEnum>(namingPolicy, tryOverrideName).ToArray();
            this.NameLookup = JsonEnumExtensions.GetLookupTable<TEnum>(this.EnumData);
        }

        public sealed override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
        {
            // Todo: consider caching a small number of JsonEncodedText values for the first N enums encountered, as is done in 
            // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
            if (TryFormatAsString(EnumData, value, out var name))
                writer.WriteStringValue(name.Span);
            else
            {
                if (!AllowNumbers)
                    throw new JsonException();
                WriteEnumAsNumber(writer, value);
            }
        }

        protected abstract bool TryFormatAsString(EnumData<TEnum> [] enumData, TEnum value, out ReadOnlyMemory<char> name);

        protected abstract bool TryReadAsString(EnumData<TEnum> [] enumData, ILookup<ReadOnlyMemory<char>, int> nameLookup, ReadOnlyMemory<char> name, out TEnum value);

        public sealed override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
            reader.TokenType switch
            {
                JsonTokenType.String => TryReadAsString(EnumData, NameLookup, reader.GetString().AsMemory(), out var value) ? value : throw new JsonException(),
                JsonTokenType.Number => AllowNumbers ? ReadNumberAsEnum(ref reader) : throw new JsonException(),
                _ => throw new JsonException(),
            };

        static void WriteEnumAsNumber(Utf8JsonWriter writer, TEnum value)
        {
            switch (EnumTypeCode)
            {
                case TypeCode.SByte:
                    writer.WriteNumberValue(Unsafe.As<TEnum, SByte>(ref value));
                    break;
                case TypeCode.Int16:
                    writer.WriteNumberValue(Unsafe.As<TEnum, Int16>(ref value));
                    break;
                case TypeCode.Int32:
                    writer.WriteNumberValue(Unsafe.As<TEnum, Int32>(ref value));
                    break;
                case TypeCode.Int64:
                    writer.WriteNumberValue(Unsafe.As<TEnum, Int64>(ref value));
                    break;
                case TypeCode.Byte:
                    writer.WriteNumberValue(Unsafe.As<TEnum, Byte>(ref value));
                    break;
                case TypeCode.UInt16:
                    writer.WriteNumberValue(Unsafe.As<TEnum, UInt16>(ref value));
                    break;
                case TypeCode.UInt32:
                    writer.WriteNumberValue(Unsafe.As<TEnum, UInt32>(ref value));
                    break;
                case TypeCode.UInt64:
                    writer.WriteNumberValue(Unsafe.As<TEnum, UInt64>(ref value));
                    break;
                default:
                    throw new JsonException();
            }
        }

        static TEnum ReadNumberAsEnum(ref Utf8JsonReader reader)
        {
            switch (EnumTypeCode)
            {
                case TypeCode.SByte:
                    {
                        var i = reader.GetSByte();
                        return Unsafe.As<SByte, TEnum>(ref i);
                    };
                case TypeCode.Int16:
                    {
                        var i = reader.GetInt16();
                        return Unsafe.As<Int16, TEnum>(ref i);
                    };
                case TypeCode.Int32:
                    {
                        var i = reader.GetInt32();
                        return Unsafe.As<Int32, TEnum>(ref i);
                    };
                case TypeCode.Int64:
                    {
                        var i = reader.GetInt64();
                        return Unsafe.As<Int64, TEnum>(ref i);
                    };
                case TypeCode.Byte:
                    {
                        var i = reader.GetByte();
                        return Unsafe.As<Byte, TEnum>(ref i);
                    };
                case TypeCode.UInt16:
                    {
                        var i = reader.GetUInt16();
                        return Unsafe.As<UInt16, TEnum>(ref i);
                    };
                case TypeCode.UInt32:
                    {
                        var i = reader.GetUInt32();
                        return Unsafe.As<UInt32, TEnum>(ref i);
                    };
                case TypeCode.UInt64:
                    {
                        var i = reader.GetUInt64();
                        return Unsafe.As<UInt64, TEnum>(ref i);
                    };
                default:
                    throw new JsonException();
            }
        }
    }
}

public sealed class NullableConverterDecorator<T> : JsonConverter<T?> where T : struct
{
    // Read() and Write() are never called with null unless HandleNull is overwridden -- which it is not.
    readonly JsonConverter<T> innerConverter;
    public NullableConverterDecorator(JsonConverter<T> innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => innerConverter.Read(ref reader, Nullable.GetUnderlyingType(typeToConvert)!, options);
    public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) => innerConverter.Write(writer, value!.Value, options);
    public override bool CanConvert(Type type) => base.CanConvert(type) && innerConverter.CanConvert(Nullable.GetUnderlyingType(type)!);
}

internal readonly record struct EnumData<TEnum>(ReadOnlyMemory<char> name, TEnum value, UInt64 UInt64Value) where TEnum : struct, Enum;

internal static class JsonEnumExtensions
{
    public static bool TryGetEnumAttribute<TAttribute>(Type type, string name, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out TAttribute? attribute) where TAttribute : System.Attribute
    {
        var member = type.GetMember(name).SingleOrDefault();
        attribute = member?.GetCustomAttribute<TAttribute>(false);
        return attribute != null;
    }
    
    public static UInt64 ToUInt64<TEnum>(this TEnum value) where TEnum : struct, Enum => value.ToUInt64(Type.GetTypeCode(typeof(TEnum)));
    
    internal static UInt64 ToUInt64<TEnum>(this TEnum value, TypeCode enumTypeCode) where TEnum : struct, Enum
    {
        Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
        return enumTypeCode switch
        {
            TypeCode.SByte => unchecked((ulong)Unsafe.As<TEnum, SByte>(ref value)),
            TypeCode.Int16 => unchecked((ulong)Unsafe.As<TEnum, Int16>(ref value)),
            TypeCode.Int32 => unchecked((ulong)Unsafe.As<TEnum, Int32>(ref value)),
            TypeCode.Int64 => unchecked((ulong)Unsafe.As<TEnum, Int64>(ref value)),
            TypeCode.Byte => Unsafe.As<TEnum, Byte>(ref value),
            TypeCode.UInt16 => Unsafe.As<TEnum, UInt16>(ref value),
            TypeCode.UInt32 => Unsafe.As<TEnum, UInt32>(ref value),
            TypeCode.UInt64 => Unsafe.As<TEnum, UInt64>(ref value),
            _ => throw new ArgumentException(enumTypeCode.ToString()),
        };
    }

    public static TEnum FromUInt64<TEnum>(this UInt64 value) where TEnum : struct, Enum => value.FromUInt64<TEnum>(Type.GetTypeCode(typeof(TEnum)));
    
    internal static TEnum FromUInt64<TEnum>(this UInt64 value, TypeCode enumTypeCode) where TEnum : struct, Enum
    {
        Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
        switch (enumTypeCode)
        {
            case TypeCode.SByte:
                {
                    var i = unchecked((SByte)value);
                    return Unsafe.As<SByte, TEnum>(ref i);
                };
            case TypeCode.Int16:
                {
                    var i = unchecked((Int16)value);
                    return Unsafe.As<Int16, TEnum>(ref i);
                };
            case TypeCode.Int32:
                {
                    var i = unchecked((Int32)value);
                    return Unsafe.As<Int32, TEnum>(ref i);
                };
            case TypeCode.Int64:
                {
                    var i = unchecked((Int64)value);
                    return Unsafe.As<Int64, TEnum>(ref i);
                };
            case TypeCode.Byte:
                {
                    var i = unchecked((Byte)value);
                    return Unsafe.As<Byte, TEnum>(ref i);
                };
            case TypeCode.UInt16:
                {
                    var i = unchecked((UInt16)value);
                    return Unsafe.As<UInt16, TEnum>(ref i);
                };
            case TypeCode.UInt32:
                {
                    var i = unchecked((UInt32)value);
                    return Unsafe.As<UInt32, TEnum>(ref i);
                };
            case TypeCode.UInt64:
                {
                    var i = unchecked((UInt64)value);
                    return Unsafe.As<UInt64, TEnum>(ref i);
                };
            default:
                throw new ArgumentException(enumTypeCode.ToString());
        }
    }
    
    // Return data about the enum sorted by the binary values of the enumeration constants (that is, by their unsigned magnitude)
    internal static IEnumerable<EnumData<TEnum>> GetData<TEnum>(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName) where TEnum : struct, Enum => 
        GetData<TEnum>(namingPolicy, tryOverrideName, Type.GetTypeCode(typeof(TEnum)));

    // Return data about the enum sorted by the binary values of the enumeration constants (that is, by their unsigned magnitude)
    internal static IEnumerable<EnumData<TEnum>> GetData<TEnum>(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName, TypeCode enumTypeCode) where TEnum : struct, Enum
    {
        Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
        var names = Enum.GetNames<TEnum>();
        var values = Enum.GetValues<TEnum>();
        return names.Zip(values, (n, v) => 
            { 
                if (tryOverrideName == null || !tryOverrideName(typeof(TEnum), n, out var jsonName))
                    jsonName = (namingPolicy == null ? n.AsMemory() : namingPolicy.ConvertName(n).AsMemory());
                return new EnumData<TEnum>(jsonName, v, v.ToUInt64(enumTypeCode));
            });
    }
    
    internal static ILookup<ReadOnlyMemory<char>, int> GetLookupTable<TEnum>(EnumData<TEnum> [] namesAndValues) where TEnum : struct, Enum => 
        Enumerable.Range(0, namesAndValues.Length).ToLookup(i => namesAndValues[i].name, CharMemoryComparer.OrdinalIgnoreCase);
    
    internal static bool TryLookupBest<TEnum>(EnumData<TEnum> [] namesAndValues, ILookup<ReadOnlyMemory<char>, int> lookupTable, ReadOnlyMemory<char> name, out TEnum value) where TEnum : struct, Enum
    {
        int i = 0;
        int firstMatch = -1;
        foreach (var index in lookupTable[name])
        {
            if (firstMatch == -1)
                firstMatch = index;
            else 
            {
                if (i == 1 && MemoryExtensions.Equals(namesAndValues[firstMatch].name.Span, name.Span, StringComparison.Ordinal))
                {
                    value = namesAndValues[firstMatch].value;
                    return true;
                }
                if (MemoryExtensions.Equals(namesAndValues[index].name.Span, name.Span, StringComparison.Ordinal))
                {
                    value = namesAndValues[index].value;
                    return true;
                }
            }
            i++;
        }
        value = (firstMatch == -1 ? default : namesAndValues[firstMatch].value);
        return firstMatch != -1;
    }
}

public static class StringExtensions
{
    public static IEnumerable<ReadOnlyMemory<char>> Split(this ReadOnlyMemory<char> chars, char separator, StringSplitOptions options = StringSplitOptions.None)
    {
        int index;
        while ((index = chars.Span.IndexOf(separator)) >= 0)
        {
            var slice = chars.Slice(0, index);
            if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
                slice = slice.Trim();
            if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || slice.Length > 0)
                yield return slice;
            chars = chars.Slice(index + 1);
        }
        if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
            chars = chars.Trim();
        if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || chars.Length > 0)
            yield return chars;
    }
}

public static class ListExtensions
{
    public static int BinarySearch<TValue, TKey>(this TValue [] list, TKey key, Func<TValue, TKey, int> comparer)
    {
        if (list == null || comparer == null)
            throw new ArgumentNullException();
        int low = 0;
        int high = list.Length - 1;
        while (low <= high)
        {
            var mid = low + ((high - low) >> 1);
            var order = comparer(list[mid], key);
            if (order == 0)
                return mid;
            else if (order > 0)
                high = mid - 1;
            else
                low = mid + 1;
        }
        return ~low;
    }
    
    public static int BinarySearchFirst<TValue, TKey>(this TValue [] list, TKey key, Func<TValue, TKey, int> comparer)
    {
        int index = list.BinarySearch(key, comparer);
        for (; index > 0 && comparer(list[index-1], key) == 0; index--)
            ;
        return index;
    }
}

public class CharMemoryComparer : IEqualityComparer<ReadOnlyMemory<char>>
{
    public static CharMemoryComparer OrdinalIgnoreCase { get; } = new CharMemoryComparer(StringComparison.OrdinalIgnoreCase);
    public static CharMemoryComparer Ordinal { get; }  = new CharMemoryComparer(StringComparison.Ordinal);

    readonly StringComparison comparison;
    CharMemoryComparer(StringComparison comparison) => this.comparison = comparison;
    public bool Equals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y) => MemoryExtensions.Equals(x.Span, y.Span, comparison);
    public int GetHashCode(ReadOnlyMemory<char> obj) => String.GetHashCode(obj.Span, comparison);
}

If your enum is annotated with EnumMember attributes, use JsonEnumMemberStringEnumConverter. If annotated with JsonPropertyName attributes as in the question, use JsonPropertyNameStringEnumConverter.

Notes:

  • Round-tripping of enums when naming policies are in use is fully supported (unlike Microsoft's JsonStringEnumConverter which does not fully support naming policies when reading).

  • Handling of enum members with identical values is consistent with JsonStringEnumConverter.

  • Nullables are supported automatically, but dictionary keys are not.

Demo .NET 6 fiddle here.

Crestfallen answered 27/11, 2019 at 0:20 Comment(8)
Seems that this will be a feature in v.5. In the meatime, this lib works: nuget.org/packages/Macross.Json.ExtensionsRevitalize
Maybe this library can also help you: github.com/StefH/System.Text.Json.EnumExtensionsMarijo
Only works for Serialization :( Cant deserialize similar wayMichelemichelina
This was a great answer for how to achieve goals using .NET Core 3.1, I'll accept a new answer that provides a succinct example for .NET 5, including serialization and deserializationMyrticemyrtie
@CraigSmitham - unfortunately Support for EnumMemberAttribute in JsonConverterEnum #31081 is still open in .Net 5.0. As far as I know the only enum-related thing that got fixed is System.Text.Json: Naming policies are not applied consistently to [Flags] enum values by JsonStringEnumConverter. #31622. A roundup of all .Net 5 System.Text.Json improvements can be found hereCrestfallen
@Crestfallen :-(. Thanks for the additional contextMyrticemyrtie
@CraigSmitham - answer updated with a converter that round-trips in .NET 6.Crestfallen
God bless you for this answer. That Nuget package did the trick after way too much time spent looking through GitHub feature requestsHoulihan
U
3

For .NET5.0 or greater ,you can add a JsonConverter

public class EnumDescriptionJsonConverter : JsonConverter<ValueType>
{
    /// <inheritdoc/>
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert.IsEnum || typeToConvert.IsGenericType && typeToConvert.GenericTypeArguments.Length == 1 && typeToConvert.GenericTypeArguments[0].IsEnum;
    }
    /// <inheritdoc/>
    public override ValueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
    /// <inheritdoc/>
    public override void Write(Utf8JsonWriter writer, ValueType value, JsonSerializerOptions options)
    {
        if (value == null)
            writer.WriteNullValue();
        else
            writer.WriteStringValue(GetDescription((value as Enum)!));
    }
    /// <summary>
    /// Get description
    /// </summary>
    /// <param name="source"></param>
    /// <returns></returns>
    static string GetDescription(Enum source)
    {
        System.Reflection.FieldInfo fi = source.GetType().GetField(source.ToString());
        if (fi == null) return source.ToString();
        System.ComponentModel.DescriptionAttribute[] attributes = (System.ComponentModel.DescriptionAttribute[])fi.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);

        if (attributes != null && attributes.Length > 0) return attributes[0].Description;
        else return source.ToString();
    }
}

The model for serialization

        public class MyClass
        {
            public MyEnum MyEnumWith0 { get; set; }
            public MyEnum MyEnumWith1 { get; set; }
            public MyEnum? NullableMyEnumWith0 { get; set; }
            public MyEnum? NullableMyEnumWith1 { get; set; }
            public MyEnum? NullableMyEnumWithNull { get; set; }
            public MyEnum? NullableMyEnumWith2 { get; set; }
        }

        public enum MyEnum
        {
            [Description("Description")]
            Key = 1,
            NoDescriptionKey = 2,
        }

Used and Test

    JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions() { WriteIndented = true };
    Assert.Equal(@"{
      ""MyEnumWith0"": 0,
      ""MyEnumWith1"": 1,
      ""NullableMyEnumWith0"": 0,
      ""NullableMyEnumWith1"": 1,
      ""NullableMyEnumWithNull"": null,
      ""NullableMyEnumWith2"": 2
    }", JsonSerializer.Serialize(value, jsonSerializerOptions));
        jsonSerializerOptions = new JsonSerializerOptions() { WriteIndented = true };
        jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        Assert.Equal(@"{
      ""MyEnumWith0"": 0,
      ""MyEnumWith1"": ""Key"",
      ""NullableMyEnumWith0"": 0,
      ""NullableMyEnumWith1"": ""Key"",
      ""NullableMyEnumWithNull"": null,
      ""NullableMyEnumWith2"": ""NoDescriptionKey""
    }", JsonSerializer.Serialize(value, jsonSerializerOptions));
        jsonSerializerOptions = new JsonSerializerOptions() { WriteIndented = true };
        jsonSerializerOptions.Converters.Add(new EnumDescriptionJsonConverter());
        Assert.Equal(@"{
      ""MyEnumWith0"": ""0"",
      ""MyEnumWith1"": ""Description"",
      ""NullableMyEnumWith0"": ""0"",
      ""NullableMyEnumWith1"": ""Description"",
      ""NullableMyEnumWithNull"": null,
      ""NullableMyEnumWith2"": ""NoDescriptionKey""
    }", JsonSerializer.Serialize(value, jsonSerializerOptions));

This converter is only available for serialization

Undoing answered 20/9, 2022 at 9:37 Comment(4)
This does not work. Renders the same result as JsonStringEnumConverterFunnel
@JaimeBula I add null check and test into answer,this solution just for .net5.0 or greaterUndoing
The code here is largely okay, but I don't think you should be using [Description] as the string value is not a human-readable description of the value.Forland
Thank you! Nice and readable, still works in .Net 8. I just had to supply my own Read method.Polymerization

© 2022 - 2024 — McMap. All rights reserved.