CSharp · Console
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters;
using System.IO;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
using NUnit.Framework; // Throws NUnit.Framework.AssertionException
//https://stackoverflow.com/questions/59059989/system-text-json-how-do-i-specify-a-custom-name-for-an-enum-value/59061296#59061296
// 10-Jul-2022
#nullable enable
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 (EnumExtensions.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 (EnumExtensions.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 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;
JsonConverter enumConverter;
TryOverrideName tryOverrideName = (Type t, string n, out ReadOnlyMemory<char> o) => TryOverrideName(t, n, out o);
var converterType = typeof(JsonEnumConverter<>).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 JsonEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum: struct, Enum
{
protected static TypeCode EnumTypeCode { get; } = Type.GetTypeCode(typeof(TEnum));
private bool AllowNumbers { get; }
EnumTypeData<TEnum> EnumTypeData { get; }
public JsonEnumConverter(JsonNamingPolicy? namingPolicy, bool allowNumbers, TryOverrideName? tryOverrideName)
{
this.AllowNumbers = allowNumbers;
this.EnumTypeData = EnumTypeData<TEnum>.Create(namingPolicy, tryOverrideName);
}
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 (EnumTypeData.TryFormatAsString(value, out var name))
writer.WriteStringValue(name.Span);
else
{
if (!AllowNumbers)
throw new JsonException();
WriteEnumAsNumber(writer, value);
}
}
public sealed override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.TokenType switch
{
JsonTokenType.String => EnumTypeData.TryLookup(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)!);
}
/********************************************************/
// Begin general enum parsing code
public delegate bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName);
public class EnumTypeData<TEnum> where TEnum : struct, Enum
{
readonly record struct EnumData(ReadOnlyMemory<char> name, TEnum value, UInt64 UInt64Value);
internal static TypeCode EnumTypeCode { get; } = Type.GetTypeCode(typeof(TEnum));
static Func<EnumData, UInt64, int> EntryComparer { get; } = (item, key) => item.UInt64Value.CompareTo(key);
static bool Flagged { get; } = typeof(TEnum).IsDefined(typeof(FlagsAttribute), false);
private EnumData [] EnumDataArray { get; }
private ILookup<ReadOnlyMemory<char>, int> NameLookup { get; }
private EnumTypeData(EnumData [] enumDataArray, ILookup<ReadOnlyMemory<char>, int> nameLookup) =>
(this.EnumDataArray, this.NameLookup) = (enumDataArray ?? throw new ArgumentNullException(nameof(enumDataArray)), nameLookup ?? throw new ArgumentNullException(nameof(nameLookup)));
public static EnumTypeData<TEnum> Create(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName)
{
var enumData = GetEnumData(namingPolicy, tryOverrideName);
return new EnumTypeData<TEnum>(enumData, GetLookupTable(enumData));
}
// Return data about the enum sorted by the binary values of the enumeration constants (that is, by their unsigned magnitude)
static EnumData [] GetEnumData(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName)
{
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(jsonName, v, v.ToUInt64(EnumTypeCode));
})
.ToArray();
}
static int FindEnumDataIndex(EnumData [] enumData, TEnum value, out UInt64 UInt64value)
{
UInt64value = value.ToUInt64(EnumTypeCode);
return enumData.BinarySearchFirst(UInt64value, EntryComparer);
}
static ILookup<ReadOnlyMemory<char>, int> GetLookupTable(EnumData [] namesAndValues) =>
Enumerable.Range(0, namesAndValues.Length).ToLookup(i => namesAndValues[i].name, CharMemoryComparer.OrdinalIgnoreCase);
public bool TryFormatAsString(TEnum value, out ReadOnlyMemory<char> name)
{
if (!Flagged)
return TryFormatSimpleAsString(value, out name);
else
return TryFormatFlaggedAsString(value, out name);
}
bool TryFormatSimpleAsString(TEnum value, out ReadOnlyMemory<char> name)
{
var index = FindEnumDataIndex(EnumDataArray, value, out var _);
if (index >= 0)
{
name = EnumDataArray[index].name;
return true;
}
name = default;
return false;
}
bool TryFormatFlaggedAsString(TEnum value, out ReadOnlyMemory<char> name)
{
var index = FindEnumDataIndex(EnumDataArray, value, out var UInt64Value);
if (index >= 0)
{
// A single flag
name = EnumDataArray[index].name;
return true;
}
if (UInt64Value != 0)
{
StringBuilder? sb = null;
for (int i = (~index) - 1; i >= 0; i--)
{
if ((UInt64Value & EnumDataArray[i].UInt64Value) == EnumDataArray[i].UInt64Value && EnumDataArray[i].UInt64Value != 0)
{
if (sb == null)
{
sb = new StringBuilder();
sb.Append(EnumDataArray[i].name.Span);
}
else
{
sb.Insert(0, EnumExtensions.FlagSeparatorString);
sb.Insert(0, EnumDataArray[i].name.Span);
}
UInt64Value -= EnumDataArray[i].UInt64Value;
}
}
if (UInt64Value == 0 && sb != null)
{
name = sb.ToString().AsMemory();
return true;
}
}
name = default;
return false;
}
public bool TryLookup(ReadOnlyMemory<char> name, out TEnum value)
{
if (!Flagged)
return TryLookupSimple(name, out value);
else
return TryLookupFlagged(name, out value);
}
bool TryLookupSimple(ReadOnlyMemory<char> name, out TEnum value)
{
int i = 0;
int firstMatch = -1;
foreach (var index in NameLookup[name])
{
if (firstMatch == -1)
firstMatch = index;
else
{
if (i == 1 && MemoryExtensions.Equals(EnumDataArray[firstMatch].name.Span, name.Span, StringComparison.Ordinal))
{
value = EnumDataArray[firstMatch].value;
return true;
}
if (MemoryExtensions.Equals(EnumDataArray[index].name.Span, name.Span, StringComparison.Ordinal))
{
value = EnumDataArray[index].value;
return true;
}
}
i++;
}
value = (firstMatch == -1 ? default : EnumDataArray[firstMatch].value);
return firstMatch != -1;
}
bool TryLookupFlagged(ReadOnlyMemory<char> name, out TEnum value)
{
UInt64 UInt64Value = 0;
foreach (var slice in name.Split(EnumExtensions.FlagSeparatorChar, StringSplitOptions.TrimEntries))
{
if (TryLookupSimple(slice, out var thisValue))
UInt64Value |= thisValue.ToUInt64();
else
{
value = default;
return false;
}
}
value = UInt64Value.ToEnum<TEnum>(EnumTypeCode);
return true;
}
}
public static partial class EnumExtensions
{
public const char FlagSeparatorChar = ',';
public const string FlagSeparatorString = ", ";
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(EnumTypeData<TEnum>.EnumTypeCode);
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 ToEnum<TEnum>(this UInt64 value) where TEnum : struct, Enum => value.ToEnum<TEnum>(EnumTypeData<TEnum>.EnumTypeCode);
internal static TEnum ToEnum<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());
}
}
}
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);
}
/********************************************************/
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
}
enum LongEnum : long
{
Min = long.MinValue,
Zero = ((long)1 << 0),
One = ((long)1 << 1),
Two = ((long)1 << 2),
Seven = ((long)1 << 7),
Fourteen = ((long)1 << 14),
ThirtyOne = ((long)1 << 31),
ThirtyTwo = ((long)1 << 32),
SixtyTwo = ((long)1 << 62),
SixtyThree = ((long)1 << 63),
Max = long.MaxValue
};
enum ULongEnum : ulong
{
Min = ulong.MinValue,
Zero = ((ulong)1 << 0),
One = ((ulong)1 << 1),
Two = ((ulong)1 << 2),
Seven = ((ulong)1 << 7),
Fourteen = ((ulong)1 << 14),
ThirtyOne = ((ulong)1 << 31),
ThirtyTwo = ((ulong)1 << 32),
ThirtyThree = ((ulong)1 << 33),
Sixty = ((ulong)1 << 60),
SixtyOne = ((ulong)1 << 61),
SixtyTwo = ((ulong)1 << 62),
SixtyThree = ((ulong)1 << 63),
Max = ulong.MaxValue,
};
enum Int32Enum : Int32
{
Min = Int32.MinValue,
Max = Int32.MaxValue,
}
enum UInt32Enum : UInt32
{
Min = UInt32.MinValue,
Max = UInt32.MaxValue,
}
enum Int16Enum : Int16
{
Min = Int16.MinValue,
Zero = 0,
One = 1,
Max = Int16.MaxValue,
}
enum UInt16Enum : UInt16
{
Min = UInt16.MinValue,
Max = UInt16.MaxValue,
}
enum SByteEnum : SByte
{
Min = SByte.MinValue,
Zero = 0,
Max = SByte.MaxValue,
}
enum ByteEnum : Byte
{
Min = Byte.MinValue,
Max = Byte.MaxValue,
}
public enum DuplicateEnum : short
{
One = 1,
Two = 1,
}
public enum EnumDifferingInCase : short { AA, Aa, aa }
public enum EmptyEnum { }
[Flags]
public enum FlaggedEmptyEnum { }
[Flags]
public enum FlaggedWithComposite
{
Zero = 0,
One = (1<<0),
Two = (1<<1),
Three = One | Two,
Four = (1<<2),
Eight = (1<<3),
All = One | Two | Four | Eight,
}
[Flags]
enum FlagLongEnum : long
{
Zero = ((long)1 << 0),
One = ((long)1 << 1),
Two = ((long)1 << 2),
Seven = ((long)1 << 7),
Fourteen = ((long)1 << 14),
ThirtyOne = ((long)1 << 31),
ThirtyTwo = ((long)1 << 32),
SixtyTwo = ((long)1 << 62),
SixtyThree = ((long)1 << 63),
};
[Flags]
enum FlagULongEnum : ulong
{
Zero = ((ulong)1 << 0),
One = ((ulong)1 << 1),
Two = ((ulong)1 << 2),
Seven = ((ulong)1 << 7),
Fourteen = ((ulong)1 << 14),
ThirtyOne = ((ulong)1 << 31),
ThirtyTwo = ((ulong)1 << 32),
ThirtyThree = ((ulong)1 << 33),
Sixty = ((ulong)1 << 60),
SixtyOne = ((ulong)1 << 61),
SixtyTwo = ((ulong)1 << 62),
SixtyThree = ((ulong)1 << 63),
};
[Flags]
enum FlagsWithManyDuplicates
{
aAAA = 1,
aaaa = 1,
AAAA = 1,
aAaA = 1,
AaAa = 1,
Aaaa = 1,
AAAa = 1,
aaaA = 1,
aaAA = 1,
AAaa = 1,
}
public enum EnumMemberExample {
Trick,
Treat,
[EnumMember(Value = "Trick-Or-Treat")] // Error: Attribute 'JsonPropertyName' is not valid on this declaration type. It is only valid on 'property, indexer' declarations.
TrickOrTreat
}
public class WackyNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name) => "Wacky-" + name;
}
class TestClass
{
public static void Test()
{
TestMisc();
TestEnumMemberOverride();
TestOverride();
TestNullable();
Assert.AreEqual(TypeCode.Int64, Type.GetTypeCode(typeof(LongEnum)));
var sysOptions = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() },
};
// Check the behavior of JsonSerializer with enums with duplicate values or that differ in case
Assert.AreEqual("[\"One\",\"One\"]", JsonSerializer.Serialize(Enum.GetValues<DuplicateEnum>(), sysOptions));
Assert.AreEqual("[\"AA\",\"Aa\",\"aa\"]", JsonSerializer.Serialize(Enum.GetValues<EnumDifferingInCase>(), sysOptions));
// Check the behavior of JsonSerializer for enums with names that only differ in case.
Assert.AreEqual(EnumDifferingInCase.AA, JsonSerializer.Deserialize<EnumDifferingInCase>("\"AA\"", sysOptions));
Assert.AreEqual(EnumDifferingInCase.aa, JsonSerializer.Deserialize<EnumDifferingInCase>("\"aa\"", sysOptions));
Assert.AreEqual(EnumDifferingInCase.Aa, JsonSerializer.Deserialize<EnumDifferingInCase>("\"Aa\"", sysOptions));
Assert.AreEqual(EnumDifferingInCase.AA, JsonSerializer.Deserialize<EnumDifferingInCase>("\"aA\"", sysOptions)); // There is no EnumDifferingInCase.aA
TestUnflagged();
Console.WriteLine();
TestFlagged();
}
static void TestEnumMemberOverride()
{
var myOptions = new JsonSerializerOptions
{
Converters = { new JsonEnumMemberStringEnumConverter(null, true) },
};
var test = Enum.GetValues<EnumMemberExample>();
var json = JsonSerializer.Serialize(test, myOptions);
Assert.AreEqual("[\"Trick\",\"Treat\",\"Trick-Or-Treat\"]", json);
Console.WriteLine(json);
var myOptions2 = new JsonSerializerOptions
{
Converters = { new JsonEnumMemberStringEnumConverter(JsonNamingPolicy.CamelCase, true) },
};
var json2 = JsonSerializer.Serialize(test, myOptions2);
Assert.AreEqual("[\"trick\",\"treat\",\"Trick-Or-Treat\"]", json2);
Console.WriteLine(json2);
}
static void TestOverride()
{
var myOptions = new JsonSerializerOptions
{
Converters = { new JsonPropertyNameStringEnumConverter(null, true) },
};
var test = Enum.GetValues<Example>();
var json = JsonSerializer.Serialize(test, myOptions);
Assert.AreEqual("[\"Trick\",\"Treat\",\"Trick-Or-Treat\"]", json);
Console.WriteLine(json);
var myOptions2 = new JsonSerializerOptions
{
Converters = { new JsonPropertyNameStringEnumConverter(JsonNamingPolicy.CamelCase, true) },
};
var json2 = JsonSerializer.Serialize(test, myOptions2);
Assert.AreEqual("[\"trick\",\"treat\",\"Trick-Or-Treat\"]", json2);
Console.WriteLine(json2);
}
static void TestNullable()
{
TestNullableNoPolicy();
TestNullableWithPolicy();
}
static void TestNullableNoPolicy()
{
var myOptions = new JsonSerializerOptions
{
Converters = { new JsonPropertyNameStringEnumConverter(null, true) },
};
var json = "[null,\"Zero\",\"SixtyThree\",\"One, Two\"]";
var array = JsonSerializer.Deserialize<FlagULongEnum? []>(json, myOptions);
Assert.IsTrue(array!.SequenceEqual(new [] { (FlagULongEnum?)null, FlagULongEnum.Zero, FlagULongEnum.SixtyThree, FlagULongEnum.One | FlagULongEnum.Two }));
Assert.AreEqual(json, JsonSerializer.Serialize(array, myOptions), json);
}
static void TestNullableWithPolicy()
{
var myOptions = new JsonSerializerOptions
{
Converters = { new GeneralJsonStringEnumConverter(JsonNamingPolicy.CamelCase, true) },
};
var json = "[null,\"zero\",\"sixtyThree\",\"one, two\"]";
var array = JsonSerializer.Deserialize<FlagULongEnum? []>(json, myOptions);
Assert.IsTrue(array!.SequenceEqual(new [] { (FlagULongEnum?)null, FlagULongEnum.Zero, FlagULongEnum.SixtyThree, FlagULongEnum.One | FlagULongEnum.Two }));
Assert.AreEqual(json, JsonSerializer.Serialize(array, myOptions), json);
}
static void TestMisc()
{
Assert.AreEqual(4, ",,,".AsMemory().Split(',').Count());
Assert.AreEqual(0, ",,,".AsMemory().Split(',', StringSplitOptions.RemoveEmptyEntries).Count());
Assert.AreEqual(0, " , , , ".AsMemory().Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Count());
Assert.AreEqual(4, " , , , ".AsMemory().Split(',', StringSplitOptions.RemoveEmptyEntries).Count());
}
static void TestFlagged()
{
var sysOptions = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter(new WackyNamingPolicy()) },
};
var test = Enum.GetValues<FlaggedWithComposite>().Concat(new [] { FlaggedWithComposite.Four | FlaggedWithComposite.Two }).ToArray();
TestWriteArrayFlagged(test, null);
TestWriteArrayFlagged(new [] { default(FlaggedEmptyEnum) });
TestRoundTripFlagged<FlaggedEmptyEnum>();
TestRoundTripFlagged<FlaggedWithComposite>(test);
TestRoundTripFlagged<FlagLongEnum>();
TestRoundTripFlagged<FlagULongEnum>();
TestRoundTripFlagged<FlagsWithManyDuplicates>();
TestRoundTripWithNamingPolicyFlagged(Enum.GetValues<FlagLongEnum>(), new WackyNamingPolicy(), true);
TestRoundTripWithNamingPolicyFlagged(Enum.GetValues<FlagULongEnum>(), new WackyNamingPolicy(), true);
TestRoundTripWithNamingPolicyFlagged(Enum.GetValues<FlagsWithManyDuplicates>(), new WackyNamingPolicy(), true);
}
static void TestRoundTripFlagged<TEnum>() where TEnum : struct, Enum =>
TestRoundTripFlagged(Enum.GetValues<TEnum>());
static void TestRoundTripFlagged<TEnum>(TEnum [] values, bool print = false) where TEnum : struct, Enum
{
var json = TestWriteArrayFlagged(values);
if (print)
Console.WriteLine(json);
TestReadArrayFlagged<TEnum>(json);
}
static void TestRoundTripWithNamingPolicyFlagged<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy, bool print = false) where TEnum : struct, Enum
{
var json = TestWriteArrayFlagged(array, namingPolicy);
var myOptions = new JsonSerializerOptions
{
Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
};
if (print)
Console.WriteLine(json);
var myArray = JsonSerializer.Deserialize<TEnum []>(json, myOptions);
Assert.IsTrue(array.SequenceEqual(myArray!), json);
}
static string TestWriteArrayFlagged<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy = default) where TEnum : struct, Enum
{
var sysOptions = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter(namingPolicy) },
};
var myOptions = new JsonSerializerOptions
{
Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
};
var sysJson = JsonSerializer.Serialize(array, sysOptions);
var myJson = JsonSerializer.Serialize(array, myOptions);
Assert.AreEqual(sysJson, myJson, myJson);
return myJson;
}
static void TestReadArrayFlagged<TEnum>(string arrayJson) where TEnum : struct, Enum
{
var sysOptions = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() },
};
var myOptions = new JsonSerializerOptions
{
Converters = { new GeneralJsonStringEnumConverter(null, true) },
};
var sysArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, sysOptions);
var myArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, myOptions);
Assert.IsTrue(sysArray!.SequenceEqual(myArray!));
}
static void TestUnflagged()
{
TestRoundTrip(Enumerable.Range(-4, 20).Select(i => (LongEnum)i).ToArray());
TestRoundTripWithNamingPolicy(new [] { default(EmptyEnum), (EmptyEnum)1 }, new WackyNamingPolicy(), false);
TestRoundTrip<ByteEnum>();
TestRoundTrip<SByteEnum>();
TestRoundTrip<Int16Enum>();
TestRoundTrip<UInt16Enum>();
TestRoundTrip<Int32Enum>();
TestRoundTrip<UInt32Enum>();
TestRoundTrip<LongEnum>();
TestRoundTrip<ULongEnum>();
TestRoundTrip<DuplicateEnum>();
TestRoundTrip<EnumDifferingInCase>();
TestReadArray<EnumDifferingInCase>("[\"AA\", \"aA\", \"Aa\", \"aa\"]");
TestRoundTripWithNamingPolicy(Enum.GetValues<ULongEnum>(), new WackyNamingPolicy(), true);
TestRoundTripWithNamingPolicy(Enum.GetValues<EnumDifferingInCase>(), new WackyNamingPolicy(), true);
TestWriteArray(Enum.GetValues<EnumDifferingInCase>(), new WackyNamingPolicy());
TestRoundTripWithNamingPolicy(Enumerable.Range(-4, 20).Select(i => (LongEnum)i).ToArray(), null, true);
TestRoundTripWithNamingPolicy(Enumerable.Range(-4, 20).Select(i => (LongEnum)i).ToArray(), new WackyNamingPolicy(), false);
TestRoundTripWithNamingPolicy(Enumerable.Range(-4, 20).Select(i => (ULongEnum)i).ToArray(), new WackyNamingPolicy(), false);
}
static void TestRoundTrip<TEnum>() where TEnum : struct, Enum =>
TestRoundTrip(Enum.GetValues<TEnum>());
static void TestRoundTrip<TEnum>(TEnum [] values, bool print = false) where TEnum : struct, Enum
{
var json = TestWriteArray(values);
if (print)
Console.WriteLine(json);
TestReadArray<TEnum>(json);
}
static void TestRoundTripWithNamingPolicy<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy, bool print = false) where TEnum : struct, Enum
{
var json = TestWriteArray(array, namingPolicy);
var myOptions = new JsonSerializerOptions
{
Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
};
if (print)
Console.WriteLine(json);
var myArray = JsonSerializer.Deserialize<TEnum []>(json, myOptions);
Assert.IsTrue(array.SequenceEqual(myArray!), json);
}
static string TestWriteArray<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy = default) where TEnum : struct, Enum
{
var sysOptions = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter(namingPolicy) },
};
var myOptions = new JsonSerializerOptions
{
Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
};
var sysJson = JsonSerializer.Serialize(array, sysOptions);
var myJson = JsonSerializer.Serialize(array, myOptions);
Assert.AreEqual(sysJson, myJson, myJson);
return myJson;
}
static void TestReadArray<TEnum>(string arrayJson) where TEnum : struct, Enum
{
var sysOptions = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() },
};
var myOptions = new JsonSerializerOptions
{
Converters = { new GeneralJsonStringEnumConverter(null, true) },
};
var sysArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, sysOptions);
var myArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, myOptions);
Assert.IsTrue(sysArray!.SequenceEqual(myArray!));
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("Environment version: {0} ({1}), OS: {2}.", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine();
try
{
TestClass.Test();
}
catch (Exception ex)
{
Console.WriteLine("Failed with unhandled exception: ");
Console.WriteLine(ex);
throw;
}
}
}