使用方法システム.テキスト.Json.NET Core のシリアライザー機能で、列挙値に似たカスタム値を指定するにはどうすればよいですか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
}
ベストアンサー1
これは現在、.net-core-3.0、.net-5、.net-6.0、.net-7.0または.net-8.0。
マイクロソフトは、その無限の知恵により、関連する問題この機能を要求するのは、基本的に実装する気がないからです。そのため、独自の機能を作成する必要があります。JsonConverterFactory
属性で指定されたカスタム値名を持つ列挙型をシリアル化する、または同様のことを行うNuGetパッケージを使用する。マクロス.Json.拡張機能。
.NET 7以降で作業している場合、または以前のバージョンでカスタム名を持つ列挙型をシリアル化するだけでデシリアル化する必要がない場合JsonConverterFactory
、カスタム名は、適応するを作成することで簡単にサポートできますJsonStringEnumConverter
カスタマイズされたJsonNamingPolicy
それぞれenum
について[EnumMember(Value = "xxx")]
任意の列挙値に適用されます。
EnumMemberAttribute
これは Newtonsoft でサポートされている属性であるため選択しましたが、JsonPropertyNameAttribute
必要に応じて代わりに使用することもできます。
まず、次のコンバーターを導入します。
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);
}
次に、次のものを飾りますenum
:
public enum Example
{
Trick,
Treat,
[EnumMember(Value = "Trick-Or-Treat")]
TrickOrTreat,
}
コンバーターをスタンドアロンで次のように使用します。
var options = new JsonSerializerOptions
{
Converters = { new JsonEnumMemberStringEnumConverter() },
// Set other options as required:
WriteIndented = true,
};
var json = JsonSerializer.Serialize(values, options);
ASP.NET Coreにコンバーターを登録するには、例えば以下を参照してください。この答えにSystem.Text.Json を使用した JsonConverter と同等によるマニ・ガンダム。
ノート:
.NET 6以前では、
JsonStringEnumConverter
逆シリアル化中に命名ポリシーが無視されます。System.Text.Json: JsonStringEnumConverter はデシリアライズ中に JsonNamingPolicy を無視します #31619詳細については、プル73348。[Flags]
.Net Core 3.x では、次のような列挙型ではコンバーターが期待どおりに動作しない可能性があります。[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"
。.NET 5ではこの問題は修正されています。問題 #31622詳細については。
デモ .NET 7 フィドルここ。
もしあなたが必要ならば往復.NET 6 以前のカスタム値名を持つ列挙型汎用コンバーターとコンバーターファクトリーを最初から作成する必要があります。これは、整数値と文字列値の解析、列挙値の各コンポーネントの名前変更[Flags]
、およびすべての可能な列挙の変換を処理する必要があるため、一般的にはやや複雑です。基礎となるタイプ(byte
、、、、など)。short
int
long
ulong
JsonStringEnumMemberConverter
前述のMacross.Json.Extensions
列挙型が次のように装飾されている場合、この機能が提供されるようです。[EnumMember(Value = "custom name")]
属性; パッケージをインストールしてMacross.Json.Extensions
から、次の操作を実行します。
// 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,
}
ドキュメントを見るここ使用方法の詳細については、こちらをご覧ください。
あるいは、独自のコードを作成することもできます。1 つの可能性を以下に示します。これは .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();