.net System.Text.Json:如何为枚举值指定自定义名称?

sgtfey8w  于 2023-05-08  发布在  .NET
关注(0)|答案(3)|浏览(217)

使用.NET Core中的System.Text.Json序列化器功能,如何为枚举值指定自定义值,类似于JsonPropertyName?例如:

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
}
y53ybaqx

y53ybaqx1#

目前.net-core-3.0.net-5.net-6.0.net-7.0中不支持开箱即用。当前有一个问题 * Support for EnumMemberAttribute in JsonConverterEnum #31081 *[1]请求此功能。在此期间,您将需要创建自己的JsonConverterFactory,该JsonConverterFactory使用由属性指定的自定义值名称序列化枚举。
如果您需要 * 往返 * 一个具有自定义值名称的枚举,您将需要从头开始创建一个通用转换器+转换器工厂。这在一般情况下有点涉及,因为需要处理整数和字符串值的解析,重命名[Flags]枚举值的每个组件,以及所有可能的底层类型(byteshortintlongulong等)的枚举。

Macross.Json.Extensions中的JsonStringEnumMemberConverter在enum用[EnumMember(Value = "custom name")]属性修饰时似乎提供了此功能;安装软件包Macross.Json.Extensions,然后执行以下操作:

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

请参阅此处的文档以了解使用细节。
或者你可以自己卷。下面示出了一种可能性。它是针对.NET 6编写的,需要一些向后移植到早期版本:

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

如果您的enum使用EnumMember属性进行注解,请使用JsonEnumMemberStringEnumConverter。如果像问题中那样使用JsonPropertyName属性进行注解,请使用JsonPropertyNameStringEnumConverter
备注:

  • 完全支持使用命名策略时枚举的往返(不像Microsoft的JsonStringEnumConverter,它在阅读时不完全支持命名策略)。
  • 处理具有相同值的枚举成员与JsonStringEnumConverter一致。
  • 自动支持空值,但不支持字典键。

演示小提琴here

如果您只需要 * 序列化 * 一个具有自定义值名称的枚举,这可以通过创建一个JsonConverterFactory来更容易地完成,该JsonConverterFactory通过为每个enum类型构造一个自定义的JsonNamingPolicy来适应JsonStringEnumConverter,该JsonConverterFactory类型将查找枚举成员上是否存在[EnumMember(Value = "xxx")]属性,如果找到任何属性,则将成员名称Map到属性的值。(我选择EnumMember是因为这是Newtonsoft支持的属性。

首先,介绍以下转换器:

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

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

    public CustomJsonStringEnumConverter(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
                    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 == null ? name : underlyingNamingPolicy.ConvertName(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);
}

然后装饰你的enum

public enum Example 
{
  Trick,
  Treat,
  [EnumMember(Value = "Trick-Or-Treat")]
   TrickOrTreat,
}

并按如下方式独立使用转换器:

var options = new JsonSerializerOptions
{
    Converters = { new CustomJsonStringEnumConverter() },
    WriteIndented = true,
};
var json = JsonSerializer.Serialize(values, options);

要向www.example.com核心注册转换器asp.net,请参见例如this answer到 * JsonConverter equivalent in using System.Text.Json *,通过Mani Gandham
备注:

[Flags]
public enum Example 
{
  Trick = (1<<0),
  Treat = (1<<1),
  [EnumMember(Value = "Trick-Or-Treat")]
   TrickOrTreat = (1<<2),
}

Example.TrickOrTreat这样的简单值可以正确重命名,但是像Example.Trick | Example.TrickOrTreat这样的复合值则不能。后者的结果应该是"Trick, Trick-Or-Treat",但实际上是"Trick, TrickOrTreat"
问题的原因是每个特定枚举类型T的基础JsonConverterEnum<T>使用构造的复合名称调用ConvertName一次,而不是使用复合名称的每个组件多次。如果需要解决方法,在DictionaryLookupNamingPolicy.ConvertName()中,可以尝试将传入的名称拆分为逗号分隔的组件,重新Map每个组件,然后重新组合结果。
为了比较,Json.NET的StringEnumConverter在复合标志值的每个组件上调用等效的方法NamingStrategy.ResolvePropertyName(string name),这似乎更正确。
在.Net 5中,这是固定的,请参阅Issue #31622了解详细信息。
演示小提琴here
[1]关闭支持System.Text.Json support to System.Runtime.Serialization #29975

643ylb08

643ylb082#

对于.NET5.0或更高版本,可以添加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();
    }
}

序列化模型

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

使用和测试

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));

此转换器仅可用于序列化

6ljaweal

6ljaweal3#

.net-core-5.0asp.net-core-5.0中,Microsoft通过JsonStringEnumConverter Class添加了对反序列化/序列化枚举的支持。
像这样装饰枚举的值:

using System.Runtime.Serialization;
public enum VipStatus
{
    [EnumMember(Value = @"IS_VIP")]
    VIP = 1,

    [EnumMember(Value = @"IS_NOT_VIP")]
    NonVIP = 2,
}

给定一个像这样的类:

class MyClass {
    public VipStatus MyVipStatus { get; set; }
}

您可以使用JsonStringEnumConverter内联序列化类的示例,如下所示:

using System.Text.Json;
using System.Text.Json.Serialization;
// ...

var myObjectWithEnums = new MyClass()
{
    MyVipStatus = VipStatus.NonVIP
};

var options = new JsonSerializerOptions();

// Configures serialization to allow strings to be accepted and auto-converted to enum values.
options.Converters.Add(new JsonStringEnumConverter());

var json = JsonSerializer.Serialize(myObjectWithEnums, options);
// serialized output is: { "myVipStatus": "IS_NOT_VIP"}

如果你使用的是ASP.NET Core 5,那么你可以在启动时配置应用程序,使用JsonStringEnumConverter来序列化所有传入的请求:

public async void ConfigureServices(IServiceCollection services) {
    // ...
    services
        .AddControllers()
        .AddJsonOptions(options => {
            // Configures serialization to allow strings to be accepted and auto-converted to enum values.
            options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        }
    // ...
});

更多阅读:How to serialize and deserialize (marshal and unmarshal) JSON in .NET Core。如果你正在使用ASP.NET,那么这也是你感兴趣的:JsonSerializerOptions的Web默认值。

相关问题