diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
index 5a1a1e9d1ace6..d1394f939f1a1 100644
--- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs
+++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
@@ -1164,6 +1164,12 @@ public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy =
public sealed override bool CanConvert(System.Type typeToConvert) { throw null; }
public sealed override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
}
+ [System.AttributeUsageAttribute(System.AttributeTargets.Field, AllowMultiple=false)]
+ public partial class JsonStringEnumMemberNameAttribute : System.Attribute
+ {
+ public JsonStringEnumMemberNameAttribute(string name) { }
+ public string Name { get { throw null; } }
+ }
public enum JsonUnknownDerivedTypeHandling
{
FailSerialization = 0,
diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx
index 63b26b0fb9bbd..e5dda53afde83 100644
--- a/src/libraries/System.Text.Json/src/Resources/Strings.resx
+++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx
@@ -231,8 +231,8 @@
'{0}' is invalid within a JSON string. The string should be correctly escaped.
-
- Enum type '{0}' uses unsupported identifer name '{1}'.
+
+ Enum type '{0}' uses unsupported identifier '{1}'. It must not be null, empty, or containing leading or trailing whitespace. Flags enums must additionally not contain commas.
'{0}' is an invalid token type for the end of the JSON payload. Expected either 'EndArray' or 'EndObject'.
diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
index 736c6be2fdbfa..071f91c5aafee 100644
--- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj
+++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
@@ -266,6 +266,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
+
diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
index 4767aa16155f8..f2e9945b49c47 100644
--- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
+++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
@@ -95,11 +95,11 @@ private static bool HasCustomAttributeWithName(this MemberInfo memberInfo, strin
/// Polyfill for BindingFlags.DoNotWrapExceptions
///
public static object? CreateInstanceNoWrapExceptions(
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicConstructors)] this Type type,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] this Type type,
Type[] parameterTypes,
object?[] parameters)
{
- ConstructorInfo ctorInfo = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, parameterTypes, null)!;
+ ConstructorInfo ctorInfo = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, parameterTypes, null)!;
#if NET
return ctorInfo.Invoke(BindingFlags.DoNotWrapExceptions, null, parameters, null);
#else
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
index 8d6f19f1a37ab..d6ade630a3986 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
@@ -317,7 +317,7 @@ public static bool HasAllSet(this BitArray bitArray)
/// Gets a Regex instance for recognizing integer representations of enums.
///
public static readonly Regex IntegerRegex = CreateIntegerRegex();
- private const string IntegerRegexPattern = @"^\s*(\+|\-)?[0-9]+\s*$";
+ private const string IntegerRegexPattern = @"^\s*(?:\+|\-)?[0-9]+\s*$";
private const int IntegerRegexTimeoutMs = 200;
#if NET
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
index 6ec71f05ddb38..e5006d23b84d4 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
@@ -3,8 +3,9 @@
using System.Buffers;
using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Encodings.Web;
using System.Text.Json.Nodes;
@@ -21,169 +22,106 @@ internal sealed class EnumConverter : JsonPrimitiveConverter
private static readonly bool s_isSignedEnum = ((int)s_enumTypeCode % 2) == 1;
private static readonly bool s_isFlagsEnum = typeof(T).IsDefined(typeof(FlagsAttribute), inherit: false);
- private const string ValueSeparator = ", ";
-
private readonly EnumConverterOptions _converterOptions;
private readonly JsonNamingPolicy? _namingPolicy;
///
- /// Holds a mapping from enum value to text that might be formatted with .
+ /// Stores metadata for the individual fields declared on the enum.
+ ///
+ private readonly EnumFieldInfo[] _enumFieldInfo;
+
+ ///
+ /// Defines a case-insensitive index of enum field names to their metadata.
+ /// In case of casing conflicts, extra fields are appended to a list in the value.
+ /// This is the main dictionary that is queried by the enum parser implementation.
+ ///
+ private readonly Dictionary _enumFieldInfoIndex;
+
+ ///
+ /// Holds a cache from enum value to formatted UTF-8 text including flag combinations.
/// is as the key used rather than given measurements that
/// show private memory savings when a single type is used https://github.com/dotnet/runtime/pull/36726#discussion_r428868336.
///
private readonly ConcurrentDictionary _nameCacheForWriting;
///
- /// Holds a mapping from text that might be formatted with to enum value.
+ /// Holds a mapping from input text to enum values including flag combinations and alternative casings.
///
- private readonly ConcurrentDictionary? _nameCacheForReading;
+ private readonly ConcurrentDictionary _nameCacheForReading;
// This is used to prevent flooding the cache due to exponential bitwise combinations of flags.
// Since multiple threads can add to the cache, a few more values might be added.
private const int NameCacheSizeSoftLimit = 64;
- public override bool CanConvert(Type type)
+ public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options)
{
- return type.IsEnum;
- }
+ Debug.Assert(EnumConverterFactory.IsSupportedTypeCode(s_enumTypeCode));
- public EnumConverter(EnumConverterOptions converterOptions, JsonSerializerOptions serializerOptions)
- : this(converterOptions, namingPolicy: null, serializerOptions)
- {
- }
-
- public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions serializerOptions)
- {
_converterOptions = converterOptions;
_namingPolicy = namingPolicy;
- _nameCacheForWriting = new ConcurrentDictionary();
+ _enumFieldInfo = ResolveEnumFields(namingPolicy);
+ _enumFieldInfoIndex = new(StringComparer.OrdinalIgnoreCase);
- if (namingPolicy != null)
- {
- _nameCacheForReading = new ConcurrentDictionary();
- }
+ _nameCacheForWriting = new();
+ _nameCacheForReading = new(StringComparer.Ordinal);
-#if NET
- string[] names = Enum.GetNames();
- T[] values = Enum.GetValues();
-#else
- string[] names = Enum.GetNames(Type);
- Array values = Enum.GetValues(Type);
-#endif
- Debug.Assert(names.Length == values.Length);
-
- JavaScriptEncoder? encoder = serializerOptions.Encoder;
-
- for (int i = 0; i < names.Length; i++)
+ JavaScriptEncoder? encoder = options.Encoder;
+ foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
{
-#if NET
- T value = values[i];
-#else
- T value = (T)values.GetValue(i)!;
-#endif
- ulong key = ConvertToUInt64(value);
- string name = names[i];
-
- string jsonName = FormatJsonName(name, namingPolicy);
- _nameCacheForWriting.TryAdd(key, JsonEncodedText.Encode(jsonName, encoder));
- _nameCacheForReading?.TryAdd(jsonName, value);
+ AddToEnumFieldIndex(fieldInfo);
- // If enum contains special char, make it failed to serialize or deserialize.
- if (name.AsSpan().IndexOfAny(',', ' ') >= 0)
- {
- ThrowHelper.ThrowInvalidOperationException_InvalidEnumTypeWithSpecialChar(typeof(T), name);
- }
+ JsonEncodedText encodedName = JsonEncodedText.Encode(fieldInfo.JsonName, encoder);
+ _nameCacheForWriting.TryAdd(fieldInfo.Key, encodedName);
+ _nameCacheForReading.TryAdd(fieldInfo.JsonName, fieldInfo.Key);
}
- }
- public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- JsonTokenType token = reader.TokenType;
-
- if (token == JsonTokenType.String)
+ if (namingPolicy != null)
{
- if ((_converterOptions & EnumConverterOptions.AllowStrings) == 0)
+ // Additionally populate the field index with the default names of fields that used a naming policy.
+ // This is done to preserve backward compat: default names should still be recognized by the parser.
+ foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
{
- ThrowHelper.ThrowJsonException();
- return default;
- }
-
-#if NET
- if (TryParseEnumCore(ref reader, out T value))
-#else
- string? enumString = reader.GetString();
- if (TryParseEnumCore(enumString, out T value))
-#endif
- {
- return value;
+ if (fieldInfo.Kind is EnumFieldNameKind.NamingPolicy)
+ {
+ AddToEnumFieldIndex(new EnumFieldInfo(fieldInfo.Key, EnumFieldNameKind.Default, fieldInfo.OriginalName, fieldInfo.OriginalName));
+ }
}
-
-#if NET
- return ReadEnumUsingNamingPolicy(reader.GetString());
-#else
- return ReadEnumUsingNamingPolicy(enumString);
-#endif
}
- if (token != JsonTokenType.Number || (_converterOptions & EnumConverterOptions.AllowNumbers) == 0)
+ void AddToEnumFieldIndex(EnumFieldInfo fieldInfo)
{
- ThrowHelper.ThrowJsonException();
- return default;
+ if (!_enumFieldInfoIndex.TryAdd(fieldInfo.JsonName, fieldInfo))
+ {
+ // We have a casing conflict, append field to the existing entry.
+ EnumFieldInfo existingFieldInfo = _enumFieldInfoIndex[fieldInfo.JsonName];
+ existingFieldInfo.AppendConflictingField(fieldInfo);
+ }
}
+ }
- switch (s_enumTypeCode)
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
{
- // Switch cases ordered by expected frequency
-
- case TypeCode.Int32:
- if (reader.TryGetInt32(out int int32))
- {
- // Use Unsafe.As instead of raw pointers for .NET Standard support.
- // https://github.com/dotnet/runtime/issues/84895
- return Unsafe.As(ref int32);
- }
- break;
- case TypeCode.UInt32:
- if (reader.TryGetUInt32(out uint uint32))
- {
- return Unsafe.As(ref uint32);
- }
- break;
- case TypeCode.UInt64:
- if (reader.TryGetUInt64(out ulong uint64))
- {
- return Unsafe.As(ref uint64);
- }
- break;
- case TypeCode.Int64:
- if (reader.TryGetInt64(out long int64))
- {
- return Unsafe.As(ref int64);
- }
- break;
- case TypeCode.SByte:
- if (reader.TryGetSByte(out sbyte byte8))
- {
- return Unsafe.As(ref byte8);
- }
- break;
- case TypeCode.Byte:
- if (reader.TryGetByte(out byte ubyte8))
- {
- return Unsafe.As(ref ubyte8);
- }
- break;
- case TypeCode.Int16:
- if (reader.TryGetInt16(out short int16))
+ case JsonTokenType.String when (_converterOptions & EnumConverterOptions.AllowStrings) != 0:
+ if (TryParseEnumFromString(ref reader, out T result))
{
- return Unsafe.As(ref int16);
+ return result;
}
break;
- case TypeCode.UInt16:
- if (reader.TryGetUInt16(out ushort uint16))
+
+ case JsonTokenType.Number when (_converterOptions & EnumConverterOptions.AllowNumbers) != 0:
+ switch (s_enumTypeCode)
{
- return Unsafe.As(ref uint16);
+ case TypeCode.Int32 when reader.TryGetInt32(out int int32): return Unsafe.As(ref int32);
+ case TypeCode.UInt32 when reader.TryGetUInt32(out uint uint32): return Unsafe.As(ref uint32);
+ case TypeCode.Int64 when reader.TryGetInt64(out long int64): return Unsafe.As(ref int64);
+ case TypeCode.UInt64 when reader.TryGetUInt64(out ulong uint64): return Unsafe.As(ref uint64);
+ case TypeCode.Byte when reader.TryGetByte(out byte ubyte8): return Unsafe.As(ref ubyte8);
+ case TypeCode.SByte when reader.TryGetSByte(out sbyte byte8): return Unsafe.As(ref byte8);
+ case TypeCode.Int16 when reader.TryGetInt16(out short int16): return Unsafe.As(ref int16);
+ case TypeCode.UInt16 when reader.TryGetUInt16(out ushort uint16): return Unsafe.As(ref uint16);
}
break;
}
@@ -194,8 +132,8 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
- // If strings are allowed, attempt to write it out as a string value
- if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0)
+ EnumConverterOptions converterOptions = _converterOptions;
+ if ((converterOptions & EnumConverterOptions.AllowStrings) != 0)
{
ulong key = ConvertToUInt64(value);
@@ -205,19 +143,13 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
return;
}
- string original = value.ToString();
-
- if (IsValidIdentifier(original))
+ if (IsDefinedValueOrCombinationOfValues(key))
{
- // We are dealing with a combination of flag constants since
- // all constant values were cached during warm-up.
- Debug.Assert(original.Contains(ValueSeparator));
-
- original = FormatJsonName(original, _namingPolicy);
-
+ Debug.Assert(s_isFlagsEnum, "Should only be entered by flags enums.");
+ string stringValue = FormatEnumAsString(key, value, dictionaryKeyPolicy: null);
if (_nameCacheForWriting.Count < NameCacheSizeSoftLimit)
{
- formatted = JsonEncodedText.Encode(original, options.Encoder);
+ formatted = JsonEncodedText.Encode(stringValue, options.Encoder);
writer.WriteStringValue(formatted);
_nameCacheForWriting.TryAdd(key, formatted);
}
@@ -225,97 +157,60 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
{
// We also do not create a JsonEncodedText instance here because passing the string
// directly to the writer is cheaper than creating one and not caching it for reuse.
- writer.WriteStringValue(original);
+ writer.WriteStringValue(stringValue);
}
return;
}
}
- if ((_converterOptions & EnumConverterOptions.AllowNumbers) == 0)
+ if ((converterOptions & EnumConverterOptions.AllowNumbers) == 0)
{
ThrowHelper.ThrowJsonException();
}
- switch (s_enumTypeCode)
+ if (s_isSignedEnum)
{
- case TypeCode.Int32:
- // Use Unsafe.As instead of raw pointers for .NET Standard support.
- // https://github.com/dotnet/runtime/issues/84895
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- case TypeCode.UInt32:
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- case TypeCode.UInt64:
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- case TypeCode.Int64:
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- case TypeCode.Int16:
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- case TypeCode.UInt16:
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- case TypeCode.Byte:
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- case TypeCode.SByte:
- writer.WriteNumberValue(Unsafe.As(ref value));
- break;
- default:
- ThrowHelper.ThrowJsonException();
- break;
+ writer.WriteNumberValue(ConvertToInt64(value));
+ }
+ else
+ {
+ writer.WriteNumberValue(ConvertToUInt64(value));
}
}
internal override T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
-#if NET
- if (TryParseEnumCore(ref reader, out T value))
-#else
- string? enumString = reader.GetString();
- if (TryParseEnumCore(reader.GetString(), out T value))
-#endif
+ // NB JsonSerializerOptions.DictionaryKeyPolicy is ignored on deserialization.
+ // This is true for all converters that implement dictionary key serialization.
+
+ if (!TryParseEnumFromString(ref reader, out T result))
{
- return value;
+ ThrowHelper.ThrowJsonException();
}
-#if NET
- return ReadEnumUsingNamingPolicy(reader.GetString());
-#else
- return ReadEnumUsingNamingPolicy(enumString);
-#endif
+ return result;
}
internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
{
+ JsonNamingPolicy? dictionaryKeyPolicy = options.DictionaryKeyPolicy is { } dkp && dkp != _namingPolicy ? dkp : null;
ulong key = ConvertToUInt64(value);
- if (options.DictionaryKeyPolicy == null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted))
+ if (dictionaryKeyPolicy is null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted))
{
writer.WritePropertyName(formatted);
return;
}
- string original = value.ToString();
-
- if (IsValidIdentifier(original))
+ if (IsDefinedValueOrCombinationOfValues(key))
{
- if (options.DictionaryKeyPolicy != null)
+ Debug.Assert(s_isFlagsEnum || dictionaryKeyPolicy != null, "Should only be entered by flags enums or dictionary key policy.");
+ string stringValue = FormatEnumAsString(key, value, dictionaryKeyPolicy);
+ if (dictionaryKeyPolicy is null && _nameCacheForWriting.Count < NameCacheSizeSoftLimit)
{
- original = FormatJsonName(original, options.DictionaryKeyPolicy);
- writer.WritePropertyName(original);
- return;
- }
-
- original = FormatJsonName(original, _namingPolicy);
-
- if (_nameCacheForWriting.Count < NameCacheSizeSoftLimit)
- {
- formatted = JsonEncodedText.Encode(original, options.Encoder);
+ // Only attempt to cache if there is no dictionary key policy.
+ formatted = JsonEncodedText.Encode(stringValue, options.Encoder);
writer.WritePropertyName(formatted);
_nameCacheForWriting.TryAdd(key, formatted);
}
@@ -323,250 +218,440 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, J
{
// We also do not create a JsonEncodedText instance here because passing the string
// directly to the writer is cheaper than creating one and not caching it for reuse.
- writer.WritePropertyName(original);
+ writer.WritePropertyName(stringValue);
}
return;
}
- switch (s_enumTypeCode)
+ if (s_isSignedEnum)
{
- // Use Unsafe.As instead of raw pointers for .NET Standard support.
- // https://github.com/dotnet/runtime/issues/84895
-
- case TypeCode.Int32:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- case TypeCode.UInt32:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- case TypeCode.UInt64:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- case TypeCode.Int64:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- case TypeCode.Int16:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- case TypeCode.UInt16:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- case TypeCode.Byte:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- case TypeCode.SByte:
- writer.WritePropertyName(Unsafe.As(ref value));
- break;
- default:
- ThrowHelper.ThrowJsonException();
- break;
+ writer.WritePropertyName(ConvertToInt64(value));
+ }
+ else
+ {
+ writer.WritePropertyName(key);
}
}
- private bool TryParseEnumCore(
-#if NET
- ref Utf8JsonReader reader,
-#else
- string? source,
-#endif
- out T value)
+ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T result)
{
-#if NET
- char[]? rentedBuffer = null;
+ Debug.Assert(reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName);
+
int bufferLength = reader.ValueLength;
+ char[]? rentedBuffer = null;
+ bool success;
Span charBuffer = bufferLength <= JsonConstants.StackallocCharThreshold
? stackalloc char[JsonConstants.StackallocCharThreshold]
: (rentedBuffer = ArrayPool.Shared.Rent(bufferLength));
int charsWritten = reader.CopyString(charBuffer);
- ReadOnlySpan source = charBuffer.Slice(0, charsWritten);
+ charBuffer = charBuffer.Slice(0, charsWritten);
+#if NET9_0_OR_GREATER
+ ReadOnlySpan source = charBuffer.Trim();
+ ConcurrentDictionary.AlternateLookup> lookup = _nameCacheForReading.GetAlternateLookup>();
+#else
+ string source = ((ReadOnlySpan)charBuffer).Trim().ToString();
+ ConcurrentDictionary lookup = _nameCacheForReading;
#endif
+ if (lookup.TryGetValue(source, out ulong key))
+ {
+ result = ConvertFromUInt64(key);
+ success = true;
+ goto End;
+ }
- bool success;
- if ((_converterOptions & EnumConverterOptions.AllowNumbers) != 0 || !JsonHelpers.IntegerRegex.IsMatch(source))
+ if (JsonHelpers.IntegerRegex.IsMatch(source))
{
- // Try parsing case sensitive first
- success = Enum.TryParse(source, out value) || Enum.TryParse(source, ignoreCase: true, out value);
+ // We found an integer that is not an enum field name.
+ if ((_converterOptions & EnumConverterOptions.AllowNumbers) != 0)
+ {
+ success = Enum.TryParse(source, out result);
+ }
+ else
+ {
+ result = default;
+ success = false;
+ }
}
else
{
- success = false;
- value = default;
+ success = TryParseNamedEnum(source, out result);
}
-#if NET
+ if (success && _nameCacheForReading.Count < NameCacheSizeSoftLimit)
+ {
+ lookup.TryAdd(source, ConvertToUInt64(result));
+ }
+
+ End:
if (rentedBuffer != null)
{
- charBuffer.Slice(0, charsWritten).Clear();
+ charBuffer.Clear();
ArrayPool.Shared.Return(rentedBuffer);
}
-#endif
+
return success;
}
- internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling)
+ private bool TryParseNamedEnum(
+#if NET9_0_OR_GREATER
+ ReadOnlySpan source,
+#else
+ string source,
+#endif
+ out T result)
{
- if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0)
- {
- // This explicitly ignores the integer component in converters configured as AllowNumbers | AllowStrings
- // which is the default for JsonStringEnumConverter. This sacrifices some precision in the schema for simplicity.
+#if NET9_0_OR_GREATER
+ Dictionary.AlternateLookup> lookup = _enumFieldInfoIndex.GetAlternateLookup>();
+ ReadOnlySpan rest = source;
+#else
+ Dictionary lookup = _enumFieldInfoIndex;
+ ReadOnlySpan rest = source.AsSpan();
+#endif
+ ulong key = 0;
- if (s_isFlagsEnum)
+ do
+ {
+ ReadOnlySpan next;
+ int i = rest.IndexOf(',');
+ if (i == -1)
{
- // Do not report enum values in case of flags.
- return new() { Type = JsonSchemaType.String };
+ next = rest;
+ rest = default;
+ }
+ else
+ {
+ next = rest.Slice(0, i).TrimEnd();
+ rest = rest.Slice(i + 1).TrimStart();
}
- JsonNamingPolicy? namingPolicy = _namingPolicy;
- JsonArray enumValues = [];
-#if NET
- string[] names = Enum.GetNames();
+ if (lookup.TryGetValue(
+#if NET9_0_OR_GREATER
+ next,
#else
- string[] names = Enum.GetNames(Type);
+ next.ToString(),
#endif
-
- for (int i = 0; i < names.Length; i++)
+ out EnumFieldInfo? firstResult) &&
+ firstResult.GetMatchingField(next) is EnumFieldInfo match)
{
- JsonNode name = FormatJsonName(names[i], namingPolicy);
- enumValues.Add(name);
+ key |= match.Key;
+ continue;
}
- return new() { Enum = enumValues };
- }
+ result = default;
+ return false;
- return new() { Type = JsonSchemaType.Integer };
+ } while (!rest.IsEmpty);
+
+ result = ConvertFromUInt64(key);
+ return true;
}
- private T ReadEnumUsingNamingPolicy(string? enumString)
+ private static ulong ConvertToUInt64(T value)
{
- if (_namingPolicy == null)
+ switch (s_enumTypeCode)
{
- ThrowHelper.ThrowJsonException();
- }
+ case TypeCode.Int32 or TypeCode.UInt32: return Unsafe.As(ref value);
+ case TypeCode.Int64 or TypeCode.UInt64: return Unsafe.As(ref value);
+ case TypeCode.Int16 or TypeCode.UInt16: return Unsafe.As(ref value);
+ default:
+ Debug.Assert(s_enumTypeCode is TypeCode.SByte or TypeCode.Byte);
+ return Unsafe.As(ref value);
+ };
+ }
- if (enumString == null)
+ private static long ConvertToInt64(T value)
+ {
+ Debug.Assert(s_isSignedEnum);
+ switch (s_enumTypeCode)
{
- ThrowHelper.ThrowJsonException();
- }
+ case TypeCode.Int32: return Unsafe.As(ref value);
+ case TypeCode.Int64: return Unsafe.As(ref value);
+ case TypeCode.Int16: return Unsafe.As(ref value);
+ default:
+ Debug.Assert(s_enumTypeCode is TypeCode.SByte);
+ return Unsafe.As(ref value);
+ };
+ }
- Debug.Assert(_nameCacheForReading != null, "Enum value cache should be instantiated if a naming policy is specified.");
+ private static T ConvertFromUInt64(ulong value)
+ {
+ switch (s_enumTypeCode)
+ {
+ case TypeCode.Int32 or TypeCode.UInt32:
+ uint uintValue = (uint)value;
+ return Unsafe.As(ref uintValue);
- bool success;
+ case TypeCode.Int64 or TypeCode.UInt64:
+ ulong ulongValue = value;
+ return Unsafe.As(ref ulongValue);
+
+ case TypeCode.Int16 or TypeCode.UInt16:
+ ushort ushortValue = (ushort)value;
+ return Unsafe.As(ref ushortValue);
+
+ default:
+ Debug.Assert(s_enumTypeCode is TypeCode.SByte or TypeCode.Byte);
+ byte byteValue = (byte)value;
+ return Unsafe.As(ref byteValue);
+ };
+ }
- if (!(success = _nameCacheForReading.TryGetValue(enumString, out T value)) && enumString.Contains(ValueSeparator))
+ ///
+ /// Attempt to format the enum value as a comma-separated string of flag values, or returns false if not a valid flag combination.
+ ///
+ private string FormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictionaryKeyPolicy)
+ {
+ Debug.Assert(IsDefinedValueOrCombinationOfValues(key), "must only be invoked against valid enum values.");
+ Debug.Assert(
+ s_isFlagsEnum || (dictionaryKeyPolicy is not null && Enum.IsDefined(typeof(T), value)),
+ "must either be a flag type or computing a dictionary key policy.");
+
+ if (s_isFlagsEnum)
{
- string[] enumValues = SplitFlagsEnum(enumString);
- ulong result = 0;
+ using ValueStringBuilder sb = new(stackalloc char[JsonConstants.StackallocCharThreshold]);
+ ulong remainingBits = key;
- for (int i = 0; i < enumValues.Length; i++)
+ foreach (EnumFieldInfo enumField in _enumFieldInfo)
{
- success = _nameCacheForReading.TryGetValue(enumValues[i], out value);
- if (!success)
+ ulong fieldKey = enumField.Key;
+ if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey)
{
- break;
+ remainingBits &= ~fieldKey;
+ string name = dictionaryKeyPolicy is not null
+ ? ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind)
+ : enumField.JsonName;
+
+ if (sb.Length > 0)
+ {
+ sb.Append(", ");
+ }
+
+ sb.Append(name);
+
+ if (remainingBits == 0)
+ {
+ break;
+ }
}
-
- result |= ConvertToUInt64(value);
}
- value = (T)Enum.ToObject(typeof(T), result);
+ Debug.Assert(remainingBits == 0 && sb.Length > 0, "unexpected remaining bits or empty string.");
+ return sb.ToString();
+ }
+ else
+ {
+ Debug.Assert(dictionaryKeyPolicy != null);
- if (success && _nameCacheForReading.Count < NameCacheSizeSoftLimit)
+ foreach (EnumFieldInfo enumField in _enumFieldInfo)
{
- _nameCacheForReading[enumString] = value;
+ // Search for an exact match and apply the key policy.
+ if (enumField.Key == key)
+ {
+ return ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind);
+ }
}
+
+ Debug.Fail("should not have been reached.");
+ return null;
}
+ }
- if (!success)
+ private bool IsDefinedValueOrCombinationOfValues(ulong key)
+ {
+ if (s_isFlagsEnum)
{
- ThrowHelper.ThrowJsonException();
+ ulong remainingBits = key;
+
+ foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
+ {
+ ulong fieldKey = fieldInfo.Key;
+ if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey)
+ {
+ remainingBits &= ~fieldKey;
+
+ if (remainingBits == 0)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
+ else
+ {
+ foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
+ {
+ if (fieldInfo.Key == key)
+ {
+ return true;
+ }
+ }
- return value;
+ return false;
+ }
}
- // This method is adapted from Enum.ToUInt64 (an internal method):
- // https://github.com/dotnet/runtime/blob/bd6cbe3642f51d70839912a6a666e5de747ad581/src/libraries/System.Private.CoreLib/src/System/Enum.cs#L240-L260
- private static ulong ConvertToUInt64(object value)
+ internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling)
{
- Debug.Assert(value is T);
- ulong result = s_enumTypeCode switch
- {
- TypeCode.Int32 => (ulong)(int)value,
- TypeCode.UInt32 => (uint)value,
- TypeCode.UInt64 => (ulong)value,
- TypeCode.Int64 => (ulong)(long)value,
- TypeCode.SByte => (ulong)(sbyte)value,
- TypeCode.Byte => (byte)value,
- TypeCode.Int16 => (ulong)(short)value,
- TypeCode.UInt16 => (ushort)value,
- _ => throw new InvalidOperationException(),
- };
- return result;
+ if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0)
+ {
+ // This explicitly ignores the integer component in converters configured as AllowNumbers | AllowStrings
+ // which is the default for JsonStringEnumConverter. This sacrifices some precision in the schema for simplicity.
+
+ if (s_isFlagsEnum)
+ {
+ // Do not report enum values in case of flags.
+ return new() { Type = JsonSchemaType.String };
+ }
+
+ JsonArray enumValues = [];
+ foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
+ {
+ enumValues.Add((JsonNode)fieldInfo.JsonName);
+ }
+
+ return new() { Enum = enumValues };
+ }
+
+ return new() { Type = JsonSchemaType.Integer };
}
- private static bool IsValidIdentifier(string value)
+ private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy)
{
- // Trying to do this check efficiently. When an enum is converted to
- // string the underlying value is given if it can't find a matching
- // identifier (or identifiers in the case of flags).
- //
- // The underlying value will be given back with a digit (e.g. 0-9) possibly
- // preceded by a negative sign. Identifiers have to start with a letter
- // so we'll just pick the first valid one and check for a negative sign
- // if needed.
- return (value[0] >= 'A' &&
- (!s_isSignedEnum || !value.StartsWith(NumberFormatInfo.CurrentInfo.NegativeSign)));
+#if NET
+ string[] names = Enum.GetNames();
+ T[] values = Enum.GetValues();
+#else
+ string[] names = Enum.GetNames(typeof(T));
+ T[] values = (T[])Enum.GetValues(typeof(T));
+#endif
+ Debug.Assert(names.Length == values.Length);
+
+ Dictionary? enumMemberAttributes = null;
+ foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static))
+ {
+ if (field.GetCustomAttribute() is { } attribute)
+ {
+ (enumMemberAttributes ??= new(StringComparer.Ordinal)).Add(field.Name, attribute.Name);
+ }
+ }
+
+ var enumFields = new EnumFieldInfo[names.Length];
+ for (int i = 0; i < names.Length; i++)
+ {
+ string originalName = names[i];
+ T value = values[i];
+ ulong key = ConvertToUInt64(value);
+ EnumFieldNameKind kind;
+
+ if (enumMemberAttributes != null && enumMemberAttributes.TryGetValue(originalName, out string? attributeName))
+ {
+ originalName = attributeName;
+ kind = EnumFieldNameKind.Attribute;
+ }
+ else
+ {
+ kind = namingPolicy != null ? EnumFieldNameKind.NamingPolicy : EnumFieldNameKind.Default;
+ }
+
+ string jsonName = ResolveAndValidateJsonName(originalName, namingPolicy, kind);
+ enumFields[i] = new EnumFieldInfo(key, kind, originalName, jsonName);
+ }
+
+ return enumFields;
}
- private static string FormatJsonName(string value, JsonNamingPolicy? namingPolicy)
+ private static string ResolveAndValidateJsonName(string name, JsonNamingPolicy? namingPolicy, EnumFieldNameKind kind)
{
- if (namingPolicy is null)
+ if (kind is not EnumFieldNameKind.Attribute && namingPolicy is not null)
{
- return value;
+ // Do not apply a naming policy to names that are explicitly set via attributes.
+ // This is consistent with JsonPropertyNameAttribute semantics.
+ name = namingPolicy.ConvertName(name);
}
- string converted;
- if (!value.Contains(ValueSeparator))
+ if (string.IsNullOrEmpty(name) || char.IsWhiteSpace(name[0]) || char.IsWhiteSpace(name[name.Length - 1]) ||
+ (s_isFlagsEnum && name.AsSpan().IndexOf(',') >= 0))
+ {
+ // Reject null or empty strings or strings with leading or trailing whitespace.
+ // In the case of flags additionally reject strings containing commas.
+ ThrowHelper.ThrowInvalidOperationException_UnsupportedEnumIdentifier(typeof(T), name);
+ }
+
+ return name;
+ }
+
+ private sealed class EnumFieldInfo(ulong key, EnumFieldNameKind kind, string originalName, string jsonName)
+ {
+ private List? _conflictingFields;
+ public EnumFieldNameKind Kind { get; } = kind;
+ public ulong Key { get; } = key;
+ public string OriginalName { get; } = originalName;
+ public string JsonName { get; } = jsonName;
+
+ ///
+ /// Assuming we have field that conflicts with the current up to case sensitivity,
+ /// append it to a list of trailing values for use by the enum value parser.
+ ///
+ public void AppendConflictingField(EnumFieldInfo other)
{
- converted = namingPolicy.ConvertName(value);
- if (converted == null)
+ Debug.Assert(JsonName.Equals(other.JsonName, StringComparison.OrdinalIgnoreCase), "The conflicting entry must be equal up to case insensitivity.");
+
+ if (Kind is EnumFieldNameKind.Default || JsonName.Equals(other.JsonName, StringComparison.Ordinal))
{
- ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy);
+ // Silently discard if the preceding entry is the default or has identical name.
+ return;
}
+
+ List conflictingFields = _conflictingFields ??= [];
+
+ // Walk the existing list to ensure we do not add duplicates.
+ foreach (EnumFieldInfo conflictingField in conflictingFields)
+ {
+ if (conflictingField.Kind is EnumFieldNameKind.Default || conflictingField.JsonName.Equals(other.JsonName, StringComparison.Ordinal))
+ {
+ return;
+ }
+ }
+
+ conflictingFields.Add(other);
}
- else
+
+ public EnumFieldInfo? GetMatchingField(ReadOnlySpan input)
{
- string[] enumValues = SplitFlagsEnum(value);
+ Debug.Assert(input.Equals(JsonName.AsSpan(), StringComparison.OrdinalIgnoreCase), "Must equal the field name up to case insensitivity.");
+
+ if (Kind is EnumFieldNameKind.Default || input.SequenceEqual(JsonName.AsSpan()))
+ {
+ // Default enum names use case insensitive parsing so are always a match.
+ return this;
+ }
- for (int i = 0; i < enumValues.Length; i++)
+ if (_conflictingFields is { } conflictingFields)
{
- string name = namingPolicy.ConvertName(enumValues[i]);
- if (name == null)
+ Debug.Assert(conflictingFields.Count > 0);
+ foreach (EnumFieldInfo matchingField in conflictingFields)
{
- ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy);
+ if (matchingField.Kind is EnumFieldNameKind.Default || input.SequenceEqual(matchingField.JsonName.AsSpan()))
+ {
+ return matchingField;
+ }
}
- enumValues[i] = name;
}
- converted = string.Join(ValueSeparator, enumValues);
+ return null;
}
-
- return converted;
}
- private static string[] SplitFlagsEnum(string value)
+ private enum EnumFieldNameKind
{
- // todo: optimize implementation here by leveraging https://github.com/dotnet/runtime/issues/934.
- return value.Split(
-#if NET
- ValueSeparator
-#else
- new string[] { ValueSeparator }, StringSplitOptions.None
-#endif
- );
+ Default = 0,
+ NamingPolicy = 1,
+ Attribute = 2,
}
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverterFactory.cs
index ed49845dcfb70..26b4979f4633d 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverterFactory.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverterFactory.cs
@@ -3,12 +3,13 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Reflection;
namespace System.Text.Json.Serialization.Converters
{
- [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
internal sealed class EnumConverterFactory : JsonConverterFactory
{
+ [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
public EnumConverterFactory()
{
}
@@ -18,23 +19,48 @@ public override bool CanConvert(Type type)
return type.IsEnum;
}
+ public static bool IsSupportedTypeCode(TypeCode typeCode)
+ {
+ return typeCode is TypeCode.SByte or TypeCode.Int16 or TypeCode.Int32 or TypeCode.Int64
+ or TypeCode.Byte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCode.UInt64;
+ }
+
+ [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
+ Justification = "The constructor has been annotated with RequiredDynamicCodeAttribute.")]
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Debug.Assert(CanConvert(type));
return Create(type, EnumConverterOptions.AllowNumbers, namingPolicy: null, options);
}
- internal static JsonConverter Create(Type enumType, EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options)
+ public static JsonConverter Create(EnumConverterOptions converterOptions, JsonSerializerOptions options, JsonNamingPolicy? namingPolicy = null)
+ where T : struct, Enum
{
- return (JsonConverter)Activator.CreateInstance(
- GetEnumConverterType(enumType),
- new object?[] { converterOptions, namingPolicy, options })!;
+ if (!IsSupportedTypeCode(Type.GetTypeCode(typeof(T))))
+ {
+ // Char-backed enums are valid in IL and F# but are not supported by System.Text.Json.
+ return new UnsupportedTypeConverter();
+ }
+
+ return new EnumConverter(converterOptions, namingPolicy, options);
}
+ [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
Justification = "'EnumConverter where T : struct' implies 'T : new()', so the trimmer is warning calling MakeGenericType here because enumType's constructors are not annotated. " +
"But EnumConverter doesn't call new T(), so this is safe.")]
- [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
- private static Type GetEnumConverterType(Type enumType) => typeof(EnumConverter<>).MakeGenericType(enumType);
+ public static JsonConverter Create(Type enumType, EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options)
+ {
+ if (!IsSupportedTypeCode(Type.GetTypeCode(enumType)))
+ {
+ // Char-backed enums are valid in IL and F# but are not supported by System.Text.Json.
+ return UnsupportedTypeConverterFactory.CreateUnsupportedConverterForType(enumType);
+ }
+
+ Type converterType = typeof(EnumConverter<>).MakeGenericType(enumType);
+ return (JsonConverter)converterType.CreateInstanceNoWrapExceptions(
+ parameterTypes: [typeof(EnumConverterOptions), typeof(JsonNamingPolicy), typeof(JsonSerializerOptions)],
+ parameters: [converterOptions, namingPolicy, options])!;
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberEnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberEnumConverter.cs
index fa7c190f4f3ea..9ee2ef5eb9ae7 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberEnumConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNumberEnumConverter.cs
@@ -33,7 +33,7 @@ public JsonNumberEnumConverter() { }
ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
}
- return new EnumConverter(EnumConverterOptions.AllowNumbers, options);
+ return EnumConverterFactory.Create(EnumConverterOptions.AllowNumbers, options);
}
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs
index 9a74546b87238..384f1336cc85b 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs
@@ -57,7 +57,7 @@ public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allow
ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
}
- return new EnumConverter(_converterOptions, _namingPolicy, options);
+ return EnumConverterFactory.Create(_converterOptions, options, _namingPolicy);
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumMemberNameAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumMemberNameAttribute.cs
new file mode 100644
index 0000000000000..192588e2909b8
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumMemberNameAttribute.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json.Serialization
+{
+ ///
+ /// Determines the string value that should be used when serializing an enum member.
+ ///
+ [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
+ public class JsonStringEnumMemberNameAttribute : Attribute
+ {
+ ///
+ /// Creates new attribute instance with a specified enum member name.
+ ///
+ /// The name to apply to the current enum member.
+ public JsonStringEnumMemberNameAttribute(string name)
+ {
+ Name = name;
+ }
+
+ ///
+ /// Gets the name of the enum member.
+ ///
+ public string Name { get; }
+ }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs
index 9c6cb4b9e3ce3..d260844f6c05d 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs
@@ -287,7 +287,7 @@ public static JsonConverter GetEnumConverter(JsonSerializerOptions options
ThrowHelper.ThrowArgumentNullException(nameof(options));
}
- return new EnumConverter(EnumConverterOptions.AllowNumbers, options);
+ return EnumConverterFactory.Create(EnumConverterOptions.AllowNumbers, options);
}
///
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
index a793d42abdaea..fed9efb84d031 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
@@ -910,9 +910,9 @@ public static void ThrowInvalidOperationException_PolymorphicTypeConfigurationDo
}
[DoesNotReturn]
- public static void ThrowInvalidOperationException_InvalidEnumTypeWithSpecialChar(Type enumType, string enumName)
+ public static void ThrowInvalidOperationException_UnsupportedEnumIdentifier(Type enumType, string? enumName)
{
- throw new InvalidOperationException(SR.Format(SR.InvalidEnumTypeWithSpecialChar, enumType.Name, enumName));
+ throw new InvalidOperationException(SR.Format(SR.UnsupportedEnumIdentifier, enumType.Name, enumName));
}
[DoesNotReturn]
diff --git a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.KeyPolicy.cs b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.KeyPolicy.cs
index ed7aad9556f67..c0f831383549c 100644
--- a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.KeyPolicy.cs
+++ b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.KeyPolicy.cs
@@ -253,7 +253,6 @@ public async Task EnumSerialization_DictionaryPolicy_NotApplied_WhenEnumsAreSeri
Assert.Equal("2", value);
-
value = await Serializer.SerializeWrapper(new ClassWithEnumProperties(), options);
Assert.Equal("{\"TestEnumProperty1\":2,\"TestEnumProperty2\":1}", value);
@@ -280,7 +279,7 @@ public async Task EnumSerialization_DictionaryPolicy_ThrowsException_WhenNamingP
InvalidOperationException ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(dict, options));
- Assert.Contains(typeof(CustomJsonNamingPolicy).ToString(), ex.Message);
+ Assert.Contains("uses unsupported identifier", ex.Message);
}
[Fact]
diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs
index d3aa23ea94851..35949a9a68a30 100644
--- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs
+++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs
@@ -93,6 +93,10 @@ public static IEnumerable GetTestDataCore()
yield return new TestData(IntEnum.A, ExpectedJsonSchema: """{"type":"integer"}""");
yield return new TestData(StringEnum.A, ExpectedJsonSchema: """{"enum":["A","B","C"]}""");
yield return new TestData(FlagsStringEnum.A, ExpectedJsonSchema: """{"type":"string"}""");
+ yield return new TestData(
+ EnumWithNameAttributes.Value1,
+ AdditionalValues: [EnumWithNameAttributes.Value2],
+ ExpectedJsonSchema: """{"enum":["A","B"]}""");
// Nullable types
yield return new TestData(true, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["boolean","null"]}""");
@@ -1077,6 +1081,15 @@ public enum StringEnum { A, B, C };
[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
public enum FlagsStringEnum { A = 1, B = 2, C = 4 };
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum EnumWithNameAttributes
+ {
+ [JsonStringEnumMemberName("A")]
+ Value1 = 1,
+ [JsonStringEnumMemberName("B")]
+ Value2 = 2,
+ }
+
public class SimplePoco
{
public string String { get; set; } = "default";
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/EnumTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/EnumTests.fs
index 07f1e2dc6a3da..98a140746c5f2 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/EnumTests.fs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/EnumTests.fs
@@ -34,16 +34,14 @@ optionsDisableNumeric.Converters.Add(new JsonStringEnumConverter(null, false))
[]
let ``Deserialize With Exception If Enum Contains Special Char`` () =
- let ex = Assert.Throws(fun () -> JsonSerializer.Deserialize(badEnumJsonStr, options) |> ignore)
- Assert.Equal(typeof, ex.InnerException.GetType())
- Assert.Equal("Enum type 'BadEnum' uses unsupported identifer name 'There's a comma, in my name'.", ex.InnerException.Message)
+ let ex = Assert.Throws(fun () -> JsonSerializer.Deserialize(badEnumJsonStr, options) |> ignore)
+ Assert.Contains("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message)
[]
let ``Serialize With Exception If Enum Contains Special Char`` () =
- let ex = Assert.Throws(fun () -> JsonSerializer.Serialize(badEnum, options) |> ignore)
- Assert.Equal(typeof, ex.InnerException.GetType())
- Assert.Equal("Enum type 'BadEnum' uses unsupported identifer name 'There's a comma, in my name'.", ex.InnerException.Message)
+ let ex = Assert.Throws(fun () -> JsonSerializer.Serialize(badEnum, options) |> ignore)
+ Assert.Contains("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message)
[]
let ``Successful Deserialize Normal Enum`` () =
@@ -52,15 +50,13 @@ let ``Successful Deserialize Normal Enum`` () =
[]
let ``Fail Deserialize Good Value Of Bad Enum Type`` () =
- let ex = Assert.Throws(fun () -> JsonSerializer.Deserialize(badEnumWithGoodValueJsonStr, options) |> ignore)
- Assert.Equal(typeof, ex.InnerException.GetType())
- Assert.Equal("Enum type 'BadEnum' uses unsupported identifer name 'There's a comma, in my name'.", ex.InnerException.Message)
+ let ex = Assert.Throws(fun () -> JsonSerializer.Deserialize(badEnumWithGoodValueJsonStr, options) |> ignore)
+ Assert.Contains("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message)
[]
let ``Fail Serialize Good Value Of Bad Enum Type`` () =
- let ex = Assert.Throws(fun () -> JsonSerializer.Serialize(badEnumWithGoodValue, options) |> ignore)
- Assert.Equal(typeof, ex.InnerException.GetType())
- Assert.Equal("Enum type 'BadEnum' uses unsupported identifer name 'There's a comma, in my name'.", ex.InnerException.Message)
+ let ex = Assert.Throws(fun () -> JsonSerializer.Serialize(badEnumWithGoodValue, options) |> ignore)
+ Assert.Contains("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message)
type NumericLabelEnum =
| ``1`` = 1
@@ -68,18 +64,23 @@ type NumericLabelEnum =
| ``3`` = 4
[]
-[]
-[]
-[]
[]
[]
[]
[]
-[]
[]
[]
let ``Fail Deserialize Numeric label Of Enum When Disallow Integer Values`` (numericValueJsonStr: string) =
Assert.Throws(fun () -> JsonSerializer.Deserialize(numericValueJsonStr, optionsDisableNumeric) |> ignore)
+
+[]
+[]
+[]
+[]
+[]
+let ``Successful Deserialize Numeric label Of Enum When Disallow Integer Values If Matching Integer Label`` (numericValueJsonStr: string, expectedValue: NumericLabelEnum) =
+ let actual = JsonSerializer.Deserialize(numericValueJsonStr, optionsDisableNumeric)
+ Assert.Equal(expectedValue, actual)
[]
[]
@@ -88,8 +89,24 @@ let ``Successful Deserialize Numeric label Of Enum When Allowing Integer Values`
let actual = JsonSerializer.Deserialize(numericValueJsonStr, options)
Assert.Equal(expectedEnumValue, actual)
+[]
+[]
+[]
+[]
+[]
+[]
+let ``Successful Deserialize Numeric label Of Enum But as Underlying value When Allowing Integer Values`` (numericValue: int) =
+ let actual = JsonSerializer.Deserialize($"\"{numericValue}\"", options)
+ Assert.Equal(LanguagePrimitives.EnumOfValue numericValue, actual)
+
+type CharEnum =
+ | A = 'A'
+ | B = 'B'
+ | C = 'C'
+
[]
-let ``Successful Deserialize Numeric label Of Enum But as Underlying value When Allowing Integer Values`` () =
- let actual = JsonSerializer.Deserialize("\"3\"", options)
- Assert.NotEqual(NumericLabelEnum.``3``, actual)
- Assert.Equal(LanguagePrimitives.EnumOfValue 3, actual)
\ No newline at end of file
+let ``Serializing char enums throws NotSupportedException`` () =
+ Assert.Throws(fun () -> JsonSerializer.Serialize(CharEnum.A) |> ignore) |> ignore
+ Assert.Throws(fun () -> JsonSerializer.Serialize(CharEnum.A, options) |> ignore) |> ignore
+ Assert.Throws(fun () -> JsonSerializer.Deserialize("0") |> ignore) |> ignore
+ Assert.Throws(fun () -> JsonSerializer.Deserialize("\"A\"", options) |> ignore) |> ignore
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs
index f188c0f241909..a7b0775361de9 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs
@@ -65,6 +65,7 @@ public sealed partial class JsonSchemaExporterTests_SourceGen()
[JsonSerializable(typeof(IntEnum))]
[JsonSerializable(typeof(StringEnum))]
[JsonSerializable(typeof(FlagsStringEnum))]
+ [JsonSerializable(typeof(EnumWithNameAttributes))]
// Nullable types
[JsonSerializable(typeof(bool?))]
[JsonSerializable(typeof(int?))]
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs
index 91f6473f78d71..4668dbc6260f1 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs
@@ -1,20 +1,24 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
+using Newtonsoft.Json;
using Xunit;
namespace System.Text.Json.Serialization.Tests
{
public class EnumConverterTests
{
+ private static readonly JsonSerializerOptions s_optionsWithStringEnumConverter = new() { Converters = { new JsonStringEnumConverter() } };
+ private static readonly JsonSerializerOptions s_optionsWithStringAndNoIntegerEnumConverter = new() { Converters = { new JsonStringEnumConverter(allowIntegerValues: false) } };
+
[Theory]
[InlineData(typeof(JsonStringEnumConverter), typeof(DayOfWeek))]
[InlineData(typeof(JsonStringEnumConverter), typeof(MyCustomEnum))]
@@ -832,5 +836,376 @@ private static JsonSerializerOptions CreateStringEnumOptionsForType(bool
{
return CreateStringEnumOptionsForType(typeof(TEnum), useGenericVariant, namingPolicy, allowIntegerValues);
}
+
+ [Theory]
+ [InlineData(EnumWithMemberAttributes.Value1, "CustomValue1")]
+ [InlineData(EnumWithMemberAttributes.Value2, "CustomValue2")]
+ [InlineData(EnumWithMemberAttributes.Value3, "Value3")]
+ public static void EnumWithMemberAttributes_StringEnumConverter_SerializesAsExpected(EnumWithMemberAttributes value, string expectedJson)
+ {
+ string json = JsonSerializer.Serialize(value, s_optionsWithStringEnumConverter);
+ Assert.Equal($"\"{expectedJson}\"", json);
+ Assert.Equal(value, JsonSerializer.Deserialize(json, s_optionsWithStringEnumConverter));
+ }
+
+ [Theory]
+ [InlineData(EnumWithMemberAttributes.Value1)]
+ [InlineData(EnumWithMemberAttributes.Value2)]
+ [InlineData(EnumWithMemberAttributes.Value3)]
+ public static void EnumWithMemberAttributes_NoStringEnumConverter_SerializesAsNumber(EnumWithMemberAttributes value)
+ {
+ string json = JsonSerializer.Serialize(value);
+ Assert.Equal($"{(int)value}", json);
+ Assert.Equal(value, JsonSerializer.Deserialize(json));
+ }
+
+ [Theory]
+ [InlineData(EnumWithMemberAttributes.Value1, "CustomValue1")]
+ [InlineData(EnumWithMemberAttributes.Value2, "CustomValue2")]
+ [InlineData(EnumWithMemberAttributes.Value3, "value3")]
+ public static void EnumWithMemberAttributes_StringEnumConverterWithNamingPolicy_NotAppliedToCustomNames(EnumWithMemberAttributes value, string expectedJson)
+ {
+ JsonSerializerOptions options = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } };
+
+ string json = JsonSerializer.Serialize(value, options);
+ Assert.Equal($"\"{expectedJson}\"", json);
+ Assert.Equal(value, JsonSerializer.Deserialize(json, options));
+ }
+
+ [Fact]
+ public static void EnumWithMemberAttributes_NamingPolicyAndDictionaryKeyPolicy_NotAppliedToCustomNames()
+ {
+ JsonSerializerOptions options = new()
+ {
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
+ DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseUpper,
+ };
+
+ Dictionary value = new()
+ {
+ [EnumWithMemberAttributes.Value1] = [EnumWithMemberAttributes.Value1, EnumWithMemberAttributes.Value2, EnumWithMemberAttributes.Value3 ],
+ [EnumWithMemberAttributes.Value2] = [EnumWithMemberAttributes.Value2 ],
+ [EnumWithMemberAttributes.Value3] = [EnumWithMemberAttributes.Value3, EnumWithMemberAttributes.Value1 ],
+ };
+
+ string json = JsonSerializer.Serialize(value, options);
+ JsonTestHelper.AssertJsonEqual("""
+ {
+ "CustomValue1": ["CustomValue1", "CustomValue2", "value3"],
+ "CustomValue2": ["CustomValue2"],
+ "VALUE3": ["value3", "CustomValue1"]
+ }
+ """, json);
+ }
+
+ [Theory]
+ [InlineData("\"customvalue1\"")]
+ [InlineData("\"CUSTOMVALUE1\"")]
+ [InlineData("\"cUSTOMvALUE1\"")]
+ [InlineData("\"customvalue2\"")]
+ [InlineData("\"CUSTOMVALUE2\"")]
+ [InlineData("\"cUSTOMvALUE2\"")]
+ public static void EnumWithMemberAttributes_CustomizedValuesAreCaseSensitive(string json)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(json, s_optionsWithStringEnumConverter));
+ }
+
+ [Theory]
+ [InlineData("\"value3\"", EnumWithMemberAttributes.Value3)]
+ [InlineData("\"VALUE3\"", EnumWithMemberAttributes.Value3)]
+ [InlineData("\"vALUE3\"", EnumWithMemberAttributes.Value3)]
+ public static void EnumWithMemberAttributes_DefaultValuesAreCaseInsensitive(string json, EnumWithMemberAttributes expectedValue)
+ {
+ EnumWithMemberAttributes value = JsonSerializer.Deserialize(json, s_optionsWithStringEnumConverter);
+ Assert.Equal(expectedValue, value);
+ }
+
+ public enum EnumWithMemberAttributes
+ {
+ [JsonStringEnumMemberName("CustomValue1")]
+ Value1 = 1,
+ [JsonStringEnumMemberName("CustomValue2")]
+ Value2 = 2,
+ Value3 = 3,
+ }
+
+ [Theory]
+ [InlineData(EnumFlagsWithMemberAttributes.Value1, "A")]
+ [InlineData(EnumFlagsWithMemberAttributes.Value2, "B")]
+ [InlineData(EnumFlagsWithMemberAttributes.Value3, "C")]
+ [InlineData(EnumFlagsWithMemberAttributes.Value4, "Value4")]
+ [InlineData(EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2, "A, B")]
+ [InlineData(EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2 | EnumFlagsWithMemberAttributes.Value3 | EnumFlagsWithMemberAttributes.Value4, "A, B, C, Value4")]
+ public static void EnumFlagsWithMemberAttributes_SerializesAsExpected(EnumFlagsWithMemberAttributes value, string expectedJson)
+ {
+ string json = JsonSerializer.Serialize(value);
+ Assert.Equal($"\"{expectedJson}\"", json);
+ Assert.Equal(value, JsonSerializer.Deserialize(json));
+ }
+
+ [Fact]
+ public static void EnumFlagsWithMemberAttributes_NamingPolicyAndDictionaryKeyPolicy_NotAppliedToCustomNames()
+ {
+ JsonSerializerOptions options = new()
+ {
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
+ DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseUpper,
+ };
+
+ Dictionary value = new()
+ {
+ [EnumFlagsWithMemberAttributes.Value1] = EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2 |
+ EnumFlagsWithMemberAttributes.Value3 | EnumFlagsWithMemberAttributes.Value4,
+
+ [EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value4] = EnumFlagsWithMemberAttributes.Value3,
+ [EnumFlagsWithMemberAttributes.Value4] = EnumFlagsWithMemberAttributes.Value2,
+ };
+
+ string json = JsonSerializer.Serialize(value, options);
+ JsonTestHelper.AssertJsonEqual("""
+ {
+ "A": "A, B, C, value4",
+ "A, VALUE4": "C",
+ "VALUE4": "B"
+ }
+ """, json);
+ }
+
+ [Theory]
+ [InlineData("\"a\"")]
+ [InlineData("\"b\"")]
+ [InlineData("\"A, b\"")]
+ [InlineData("\"A, b, C, Value4\"")]
+ [InlineData("\"A, B, c, Value4\"")]
+ [InlineData("\"a, b, c, Value4\"")]
+ [InlineData("\"c, B, A, Value4\"")]
+ public static void EnumFlagsWithMemberAttributes_CustomizedValuesAreCaseSensitive(string json)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(json));
+ }
+
+ [Theory]
+ [InlineData("\"value4\"", EnumFlagsWithMemberAttributes.Value4)]
+ [InlineData("\"value4, VALUE4\"", EnumFlagsWithMemberAttributes.Value4)]
+ [InlineData("\"A, value4, VALUE4, A,B,A,A\"", EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2 | EnumFlagsWithMemberAttributes.Value4)]
+ [InlineData("\"VALUE4, VAlUE5\"", EnumFlagsWithMemberAttributes.Value4 | EnumFlagsWithMemberAttributes.Value5)]
+ public static void EnumFlagsWithMemberAttributes_DefaultValuesAreCaseInsensitive(string json, EnumFlagsWithMemberAttributes expectedValue)
+ {
+ EnumFlagsWithMemberAttributes value = JsonSerializer.Deserialize(json);
+ Assert.Equal(expectedValue, value);
+ }
+
+ [Flags, JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum EnumFlagsWithMemberAttributes
+ {
+ [JsonStringEnumMemberName("A")]
+ Value1 = 1,
+ [JsonStringEnumMemberName("B")]
+ Value2 = 2,
+ [JsonStringEnumMemberName("C")]
+ Value3 = 4,
+ Value4 = 8,
+ Value5 = 16,
+ }
+
+ [Theory]
+ [InlineData(EnumWithConflictingMemberAttributes.Value1)]
+ [InlineData(EnumWithConflictingMemberAttributes.Value2)]
+ [InlineData(EnumWithConflictingMemberAttributes.Value3)]
+ public static void EnumWithConflictingMemberAttributes_IsTolerated(EnumWithConflictingMemberAttributes value)
+ {
+ string json = JsonSerializer.Serialize(value);
+ Assert.Equal("\"Value3\"", json);
+ Assert.Equal(EnumWithConflictingMemberAttributes.Value1, JsonSerializer.Deserialize(json));
+ }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum EnumWithConflictingMemberAttributes
+ {
+ [JsonStringEnumMemberName("Value3")]
+ Value1 = 1,
+ [JsonStringEnumMemberName("Value3")]
+ Value2 = 2,
+ Value3 = 3,
+ }
+
+ [Theory]
+ [InlineData(EnumWithConflictingCaseNames.ValueWithConflictingCase, "\"ValueWithConflictingCase\"")]
+ [InlineData(EnumWithConflictingCaseNames.VALUEwithCONFLICTINGcase, "\"VALUEwithCONFLICTINGcase\"")]
+ [InlineData(EnumWithConflictingCaseNames.Value3, "\"VALUEWITHCONFLICTINGCASE\"")]
+ public static void EnumWithConflictingCaseNames_SerializesAsExpected(EnumWithConflictingCaseNames value, string expectedJson)
+ {
+ string json = JsonSerializer.Serialize(value);
+ Assert.Equal(expectedJson, json);
+ EnumWithConflictingCaseNames deserializedValue = JsonSerializer.Deserialize(json);
+ Assert.Equal(value, deserializedValue);
+ }
+
+ [Theory]
+ [InlineData("\"valuewithconflictingcase\"")]
+ [InlineData("\"vALUEwITHcONFLICTINGcASE\"")]
+ public static void EnumWithConflictingCaseNames_DeserializingMismatchingCaseDefaultsToFirstValue(string json)
+ {
+ EnumWithConflictingCaseNames value = JsonSerializer.Deserialize(json);
+ Assert.Equal(EnumWithConflictingCaseNames.ValueWithConflictingCase, value);
+ }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum EnumWithConflictingCaseNames
+ {
+ ValueWithConflictingCase = 1,
+ VALUEwithCONFLICTINGcase = 2,
+ [JsonStringEnumMemberName("VALUEWITHCONFLICTINGCASE")]
+ Value3 = 3,
+ }
+
+ [Theory]
+ [InlineData(EnumWithValidMemberNames.Value1, "\"Intermediate whitespace\\t is allowed\\r\\nin enums\"")]
+ [InlineData(EnumWithValidMemberNames.Value2, "\"Including support for commas, and other punctuation.\"")]
+ [InlineData(EnumWithValidMemberNames.Value3, "\"Nice \\uD83D\\uDE80\\uD83D\\uDE80\\uD83D\\uDE80\"")]
+ [InlineData(EnumWithValidMemberNames.Value4, "\"5\"")]
+ [InlineData(EnumWithValidMemberNames.Value1 | EnumWithValidMemberNames.Value4, "5")]
+ public static void EnumWithValidMemberNameOverrides(EnumWithValidMemberNames value, string expectedJsonString)
+ {
+ string json = JsonSerializer.Serialize(value);
+ Assert.Equal(expectedJsonString, json);
+ Assert.Equal(value, JsonSerializer.Deserialize(json));
+ }
+
+ [Fact]
+ public static void EnumWithNumberIdentifier_CanDeserializeAsUnderlyingValue()
+ {
+ EnumWithValidMemberNames value = JsonSerializer.Deserialize("\"4\"");
+ Assert.Equal(EnumWithValidMemberNames.Value4, value);
+ }
+
+ [Fact]
+ public static void EnumWithNumberIdentifier_NoNumberSupported_FailsWhenDeserializingUnderlyingValue()
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize("\"4\"", s_optionsWithStringAndNoIntegerEnumConverter));
+ }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum EnumWithValidMemberNames
+ {
+ [JsonStringEnumMemberName("Intermediate whitespace\t is allowed\r\nin enums")]
+ Value1 = 1,
+
+ [JsonStringEnumMemberName("Including support for commas, and other punctuation.")]
+ Value2 = 2,
+
+ [JsonStringEnumMemberName("Nice 🚀🚀🚀")]
+ Value3 = 3,
+
+ [JsonStringEnumMemberName("5")]
+ Value4 = 4
+ }
+
+ [Theory]
+ [InlineData(EnumFlagsWithValidMemberNames.Value1 | EnumFlagsWithValidMemberNames.Value2, "\"Intermediate whitespace\\t is allowed\\r\\nin enums, Including support for some punctuation; except commas.\"")]
+ [InlineData(EnumFlagsWithValidMemberNames.Value3 | EnumFlagsWithValidMemberNames.Value4, "\"Nice \\uD83D\\uDE80\\uD83D\\uDE80\\uD83D\\uDE80, 5\"")]
+ [InlineData(EnumFlagsWithValidMemberNames.Value4, "\"5\"")]
+ public static void EnumFlagsWithValidMemberNameOverrides(EnumFlagsWithValidMemberNames value, string expectedJsonString)
+ {
+ string json = JsonSerializer.Serialize(value);
+ Assert.Equal(expectedJsonString, json);
+ Assert.Equal(value, JsonSerializer.Deserialize(json));
+ }
+
+ [Theory]
+ [InlineData("\"\\r\\n Intermediate whitespace\\t is allowed\\r\\nin enums , Including support for some punctuation; except commas.\\r\\n\"", EnumFlagsWithValidMemberNames.Value1 | EnumFlagsWithValidMemberNames.Value2)]
+ [InlineData("\" 5\\t, \\r\\n 5,\\t 5\"", EnumFlagsWithValidMemberNames.Value4)]
+ public static void EnumFlagsWithValidMemberNameOverrides_SupportsWhitespaceSeparatedValues(string json, EnumFlagsWithValidMemberNames expectedValue)
+ {
+ EnumFlagsWithValidMemberNames result = JsonSerializer.Deserialize(json);
+ Assert.Equal(expectedValue, result);
+ }
+
+ [Theory]
+ [InlineData("\"\"")]
+ [InlineData("\" \\r\\n \"")]
+ [InlineData("\",\"")]
+ [InlineData("\",,,\"")]
+ [InlineData("\", \\r\\n,,\"")]
+ [InlineData("\"\\r\\n Intermediate whitespace\\t is allowed\\r\\nin enums , 13 , Including support for some punctuation; except commas.\\r\\n\"")]
+ [InlineData("\"\\r\\n Intermediate whitespace\\t is allowed\\r\\nin enums , , Including support for some punctuation; except commas.\\r\\n\"")]
+ [InlineData("\" 5\\t, \\r\\n , UNKNOWN_IDENTIFIER \r\n, 5,\\t 5\"")]
+ public static void EnumFlagsWithValidMemberNameOverrides_FailsOnInvalidJsonValues(string json)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(json));
+ }
+
+ [Flags, JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum EnumFlagsWithValidMemberNames
+ {
+ [JsonStringEnumMemberName("Intermediate whitespace\t is allowed\r\nin enums")]
+ Value1 = 1,
+
+ [JsonStringEnumMemberName("Including support for some punctuation; except commas.")]
+ Value2 = 2,
+
+ [JsonStringEnumMemberName("Nice 🚀🚀🚀")]
+ Value3 = 4,
+
+ [JsonStringEnumMemberName("5")]
+ Value4 = 8
+ }
+
+ [Theory]
+ [InlineData(typeof(EnumWithInvalidMemberName1), "")]
+ [InlineData(typeof(EnumWithInvalidMemberName2), "")]
+ [InlineData(typeof(EnumWithInvalidMemberName3), " ")]
+ [InlineData(typeof(EnumWithInvalidMemberName4), " HasLeadingWhitespace")]
+ [InlineData(typeof(EnumWithInvalidMemberName5), "HasTrailingWhitespace\n")]
+ [InlineData(typeof(EnumWithInvalidMemberName6), "Comma separators not allowed, in flags enums")]
+ public static void EnumWithInvalidMemberName_Throws(Type enumType, string memberName)
+ {
+ object value = Activator.CreateInstance(enumType);
+ string expectedExceptionMessage = $"Enum type '{enumType.Name}' uses unsupported identifier '{memberName}'.";
+ InvalidOperationException ex;
+
+ ex = Assert.Throws(() => JsonSerializer.Serialize(value, enumType, s_optionsWithStringEnumConverter));
+ Assert.Contains(expectedExceptionMessage, ex.Message);
+
+ ex = Assert.Throws(() => JsonSerializer.Deserialize("\"str\"", enumType, s_optionsWithStringEnumConverter));
+ Assert.Contains(expectedExceptionMessage, ex.Message);
+ }
+
+ public enum EnumWithInvalidMemberName1
+ {
+ [JsonStringEnumMemberName(null!)]
+ Value
+ }
+
+ public enum EnumWithInvalidMemberName2
+ {
+ [JsonStringEnumMemberName("")]
+ Value
+ }
+
+ public enum EnumWithInvalidMemberName3
+ {
+ [JsonStringEnumMemberName(" ")]
+ Value
+ }
+
+ public enum EnumWithInvalidMemberName4
+ {
+ [JsonStringEnumMemberName(" HasLeadingWhitespace")]
+ Value
+ }
+
+ public enum EnumWithInvalidMemberName5
+ {
+ [JsonStringEnumMemberName("HasTrailingWhitespace\n")]
+ Value
+ }
+
+ [Flags]
+ public enum EnumWithInvalidMemberName6
+ {
+ [JsonStringEnumMemberName("Comma separators not allowed, in flags enums")]
+ Value
+ }
}
}
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/EnumConverterTest.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/EnumConverterTest.cs
index 96c5986749a8a..290c108e1d80b 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/EnumConverterTest.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/EnumConverterTest.cs
@@ -4,6 +4,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
+using System.Text.Json.Serialization;
namespace SerializerTrimmingTest
{
@@ -16,7 +17,18 @@ internal class Program
static int Main(string[] args)
{
string json = JsonSerializer.Serialize(new ClassWithDay());
- return json == @"{""Day"":0}" ? 100 : -1;
+ if (json != """{"Day":0}""")
+ {
+ return -1;
+ }
+
+ json = JsonSerializer.Serialize(new ClassWithDaySourceGen(), Context.Default.ClassWithDaySourceGen);
+ if (json != """{"Day":"Sun"}""")
+ {
+ return -2;
+ }
+
+ return 100;
}
}
@@ -24,4 +36,31 @@ internal class ClassWithDay
{
public DayOfWeek Day { get; set; }
}
+
+ internal class ClassWithDaySourceGen
+ {
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public DayOfWeek Day { get; set; }
+ }
+
+ internal enum DayOfWeek
+ {
+ [JsonStringEnumMemberName("Sun")]
+ Sunday,
+ [JsonStringEnumMemberName("Mon")]
+ Monday,
+ [JsonStringEnumMemberName("Tue")]
+ Tuesday,
+ [JsonStringEnumMemberName("Wed")]
+ Wednesday,
+ [JsonStringEnumMemberName("Thu")]
+ Thursday,
+ [JsonStringEnumMemberName("Fri")]
+ Friday,
+ [JsonStringEnumMemberName("Sat")]
+ Saturday
+ }
+
+ [JsonSerializable(typeof(ClassWithDaySourceGen))]
+ internal partial class Context : JsonSerializerContext;
}