From f3f249f13b1ace8f64a3dba352a9e6c2b2220115 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 17 Jul 2024 16:29:42 +0100 Subject: [PATCH 01/11] Add JsonEnumMemberNameAttribute. --- .../System.Text.Json/ref/System.Text.Json.cs | 6 + .../src/System.Text.Json.csproj | 1 + .../Converters/Value/EnumConverter.cs | 444 +++++++++--------- .../Converters/Value/EnumConverterFactory.cs | 33 +- .../Serialization/JsonNumberEnumConverter.cs | 2 +- .../Serialization/JsonStringEnumConverter.cs | 2 +- .../JsonStringEnumMemberNameAttribute.cs | 31 ++ .../JsonMetadataServices.Converters.cs | 2 +- .../JsonSchemaExporterTests.TestTypes.cs | 13 + .../EnumTests.fs | 14 +- .../Serialization/JsonSchemaExporterTests.cs | 1 + .../Serialization/EnumConverterTests.cs | 91 +++- .../TrimmingTests/EnumConverterTest.cs | 41 +- 13 files changed, 459 insertions(+), 222 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumMemberNameAttribute.cs 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/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 28aef5494651c..a858ead3425b2 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/Text/Json/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs index 6ec71f05ddb38..c6ba48375ba36 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 @@ -2,9 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Json.Nodes; @@ -16,9 +19,6 @@ internal sealed class EnumConverter : JsonPrimitiveConverter where T : struct, Enum { private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(typeof(T)); - - // Odd type codes are conveniently signed types (for enum backing types). - 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 = ", "; @@ -27,6 +27,16 @@ internal sealed class EnumConverter : JsonPrimitiveConverter private readonly JsonNamingPolicy? _namingPolicy; + /// + /// Whether either of the enum fields have been overridden with . + /// + private readonly bool _containsNameAttributes; + + /// + /// Stores metadata for the individual fields declared on the enum. + /// + private readonly EnumFieldInfo[] _enumFieldInfo; + /// /// Holds a mapping from enum value to text that might be formatted with . /// is as the key used rather than given measurements that @@ -43,57 +53,25 @@ internal sealed class EnumConverter : JsonPrimitiveConverter // 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) - { - return type.IsEnum; - } - - public EnumConverter(EnumConverterOptions converterOptions, JsonSerializerOptions serializerOptions) - : this(converterOptions, namingPolicy: null, serializerOptions) - { - } - public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions serializerOptions) { + Debug.Assert(s_enumTypeCode is TypeCode.SByte or TypeCode.Int16 or TypeCode.Int32 or TypeCode.Int64 + or TypeCode.Byte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCode.UInt64); + _converterOptions = converterOptions; _namingPolicy = namingPolicy; - _nameCacheForWriting = new ConcurrentDictionary(); - - if (namingPolicy != null) + _enumFieldInfo = ResolveEnumFields(namingPolicy, out _containsNameAttributes); + _nameCacheForWriting = new(); + if (namingPolicy != null || _containsNameAttributes) { - _nameCacheForReading = new ConcurrentDictionary(); + _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++) + 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); - - // If enum contains special char, make it failed to serialize or deserialize. - if (name.AsSpan().IndexOfAny(',', ' ') >= 0) - { - ThrowHelper.ThrowInvalidOperationException_InvalidEnumTypeWithSpecialChar(typeof(T), name); - } + _nameCacheForWriting.TryAdd(fieldInfo.Key, JsonEncodedText.Encode(fieldInfo.Name, encoder)); + _nameCacheForReading?.TryAdd(fieldInfo.Name, fieldInfo.Value); } } @@ -103,33 +81,18 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial if (token == JsonTokenType.String) { - if ((_converterOptions & EnumConverterOptions.AllowStrings) == 0) - { - 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 + if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0 && + TryParseEnumFromString(ref reader, out T value)) { return value; } -#if NET - return ReadEnumUsingNamingPolicy(reader.GetString()); -#else - return ReadEnumUsingNamingPolicy(enumString); -#endif + ThrowHelper.ThrowJsonException(); } if (token != JsonTokenType.Number || (_converterOptions & EnumConverterOptions.AllowNumbers) == 0) { ThrowHelper.ThrowJsonException(); - return default; } switch (s_enumTypeCode) @@ -205,19 +168,13 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions return; } - string original = value.ToString(); - - if (IsValidIdentifier(original)) + if (TryFormatEnumAsString(value, dictionaryKeyPolicy: null, out string? stringValue, out bool _)) { - // 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, "path should only be entered by flag enums."); if (_nameCacheForWriting.Count < NameCacheSizeSoftLimit) { - formatted = JsonEncodedText.Encode(original, options.Encoder); + formatted = JsonEncodedText.Encode(stringValue, options.Encoder); writer.WriteStringValue(formatted); _nameCacheForWriting.TryAdd(key, formatted); } @@ -225,7 +182,7 @@ 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; @@ -273,49 +230,29 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions 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 + if (!TryParseEnumFromString(ref reader, out T value)) { - return value; + ThrowHelper.ThrowJsonException(); } -#if NET - return ReadEnumUsingNamingPolicy(reader.GetString()); -#else - return ReadEnumUsingNamingPolicy(enumString); -#endif + return value; } internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, JsonSerializerOptions options, bool isWritingExtensionDataProperty) { ulong key = ConvertToUInt64(value); - if (options.DictionaryKeyPolicy == null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted)) + if (options.DictionaryKeyPolicy is null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted)) { writer.WritePropertyName(formatted); return; } - string original = value.ToString(); - - if (IsValidIdentifier(original)) + if (TryFormatEnumAsString(value, options.DictionaryKeyPolicy, out string? stringEnum, out bool isDictionaryKeyPolicyApplied)) { - if (options.DictionaryKeyPolicy != null) - { - original = FormatJsonName(original, options.DictionaryKeyPolicy); - writer.WritePropertyName(original); - return; - } - - original = FormatJsonName(original, _namingPolicy); - - if (_nameCacheForWriting.Count < NameCacheSizeSoftLimit) + if (!isDictionaryKeyPolicyApplied && _nameCacheForWriting.Count < NameCacheSizeSoftLimit) { - formatted = JsonEncodedText.Encode(original, options.Encoder); + formatted = JsonEncodedText.Encode(stringEnum, options.Encoder); writer.WritePropertyName(formatted); _nameCacheForWriting.TryAdd(key, formatted); } @@ -323,7 +260,7 @@ 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(stringEnum); } return; @@ -364,14 +301,9 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, J } } - private bool TryParseEnumCore( -#if NET - ref Utf8JsonReader reader, -#else - string? source, -#endif - out T value) + private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) { + bool success; #if NET char[]? rentedBuffer = null; int bufferLength = reader.ValueLength; @@ -381,25 +313,52 @@ private bool TryParseEnumCore( : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); int charsWritten = reader.CopyString(charBuffer); - ReadOnlySpan source = charBuffer.Slice(0, charsWritten); + Span source = charBuffer.Slice(0, charsWritten); +#else + string source = reader.GetString(); #endif + // Skip the built-in enum parser and go directly to the read cache if either: + // + // 1. one of the enum fields have had their names overridden OR + // 2. the source string represents a number when numbers are not permitted. + // + // For backward compatibility reasons the built-in parser is not skipped if a naming policy is specified. + bool skipEnumParser = _containsNameAttributes || + ((_converterOptions & EnumConverterOptions.AllowNumbers) is 0 && JsonHelpers.IntegerRegex.IsMatch(source)); - bool success; - if ((_converterOptions & EnumConverterOptions.AllowNumbers) != 0 || !JsonHelpers.IntegerRegex.IsMatch(source)) + if (!skipEnumParser && Enum.TryParse(source, ignoreCase: true, out value)) { - // Try parsing case sensitive first - success = Enum.TryParse(source, out value) || Enum.TryParse(source, ignoreCase: true, out value); + success = true; + goto End; } - else + + Debug.Assert(_nameCacheForReading is null == (_namingPolicy is null && !_containsNameAttributes), + "A read cache should only be populated if we have a naming policy or name attributes."); + + if (_nameCacheForReading is not { } nameCacheForReading) { - success = false; value = default; + success = false; + goto End; } +#if NET9_0_OR_GREATER + success = nameCacheForReading.GetAlternateLookup>().TryGetValue(source, out value) || + (source.Contains(',') && TryParseFromCommaSeparatedString(source.ToString(), out value)); +#elif NET + string enumString = source.ToString(); + success = nameCacheForReading.TryGetValue(enumString, out value) || + TryParseFromCommaSeparatedString(enumString, out value); +#else + success = nameCacheForReading.TryGetValue(source, out value) || + TryParseFromCommaSeparatedString(source, out value); +#endif + + End: #if NET if (rentedBuffer != null) { - charBuffer.Slice(0, charsWritten).Clear(); + source.Clear(); ArrayPool.Shared.Return(rentedBuffer); } #endif @@ -419,18 +378,10 @@ private bool TryParseEnumCore( return new() { Type = JsonSchemaType.String }; } - JsonNamingPolicy? namingPolicy = _namingPolicy; JsonArray enumValues = []; -#if NET - string[] names = Enum.GetNames(); -#else - string[] names = Enum.GetNames(Type); -#endif - - for (int i = 0; i < names.Length; i++) + foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) { - JsonNode name = FormatJsonName(names[i], namingPolicy); - enumValues.Add(name); + enumValues.Add((JsonNode)fieldInfo.Name); } return new() { Enum = enumValues }; @@ -439,134 +390,209 @@ private bool TryParseEnumCore( return new() { Type = JsonSchemaType.Integer }; } - private T ReadEnumUsingNamingPolicy(string? enumString) + private bool TryParseFromCommaSeparatedString(string enumString, out T value) { - if (_namingPolicy == null) - { - ThrowHelper.ThrowJsonException(); - } + Debug.Assert(_nameCacheForReading != null); - if (enumString == null) + if (!enumString.Contains(ValueSeparator)) { - ThrowHelper.ThrowJsonException(); + value = default; + return false; } - Debug.Assert(_nameCacheForReading != null, "Enum value cache should be instantiated if a naming policy is specified."); - - bool success; + ConcurrentDictionary nameCacheForReading = _nameCacheForReading; + ulong result = 0; - if (!(success = _nameCacheForReading.TryGetValue(enumString, out T value)) && enumString.Contains(ValueSeparator)) + foreach (string component in SplitFlagsEnum(enumString)) { - string[] enumValues = SplitFlagsEnum(enumString); - ulong result = 0; - - for (int i = 0; i < enumValues.Length; i++) + if (!nameCacheForReading.TryGetValue(component, out value)) { - success = _nameCacheForReading.TryGetValue(enumValues[i], out value); - if (!success) - { - break; - } - - result |= ConvertToUInt64(value); + return false; } - value = (T)Enum.ToObject(typeof(T), result); - - if (success && _nameCacheForReading.Count < NameCacheSizeSoftLimit) - { - _nameCacheForReading[enumString] = value; - } + result |= ConvertToUInt64(value); } - if (!success) + value = (T)Enum.ToObject(typeof(T), result); + + if (nameCacheForReading.Count < NameCacheSizeSoftLimit) { - ThrowHelper.ThrowJsonException(); + nameCacheForReading[enumString] = value; } - return value; + return true; } // 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) + private static ulong ConvertToUInt64(T value) { - Debug.Assert(value is T); - ulong result = s_enumTypeCode switch + return 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(), + TypeCode.Int32 or TypeCode.UInt32 => Unsafe.As(ref value), + TypeCode.Int64 or TypeCode.UInt64 => Unsafe.As(ref value), + TypeCode.Int16 or TypeCode.UInt16 => Unsafe.As(ref value), + TypeCode.Byte or TypeCode.SByte => Unsafe.As(ref value), + _ => Throw(), }; - return result; - } - private static bool IsValidIdentifier(string value) - { - // 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))); + static ulong Throw() => throw new InvalidOperationException(); } - private static string FormatJsonName(string value, JsonNamingPolicy? namingPolicy) + private static string ApplyNamingPolicy(string value, JsonNamingPolicy? namingPolicy) { - if (namingPolicy is null) - { - return value; - } - - string converted; - if (!value.Contains(ValueSeparator)) + if (namingPolicy is not null) { - converted = namingPolicy.ConvertName(value); - if (converted == null) + value = namingPolicy.ConvertName(value); + if (value is null) { ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy); } } - else + + return value; + } + + private static string[] SplitFlagsEnum(string value) + { + // 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 + ); + } + + /// + /// Attempt to format the enum value as a comma-separated string of flag values, or returns false if not a valid flag combination. + /// + private bool TryFormatEnumAsString( + T value, + JsonNamingPolicy? dictionaryKeyPolicy, + [NotNullWhen(true)] out string? stringValue, + out bool isDictionaryKeyPolicyApplied) + { + Debug.Assert(!Enum.IsDefined(typeof(T), value) || dictionaryKeyPolicy is not null, "Defined values must have already been handled."); + isDictionaryKeyPolicyApplied = false; + + if (s_isFlagsEnum || dictionaryKeyPolicy is not null) { - string[] enumValues = SplitFlagsEnum(value); + ulong key = ConvertToUInt64(value); + ulong remainingBits = key; + using ValueStringBuilder sb = new(stackalloc char[JsonConstants.StackallocCharThreshold]); - for (int i = 0; i < enumValues.Length; i++) + foreach (EnumFieldInfo enumField in _enumFieldInfo) { - string name = namingPolicy.ConvertName(enumValues[i]); - if (name == null) + ulong fieldKey = enumField.Key; + if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey) { - ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy); + string name; + if (dictionaryKeyPolicy != null && !enumField.IsAttributeOverriddenName) + { + name = ApplyNamingPolicy(enumField.OriginalName, dictionaryKeyPolicy); + isDictionaryKeyPolicyApplied = true; + } + else + { + name = enumField.Name; + } + + if (sb.Length > 0) + { + sb.Append(ValueSeparator); + } + + sb.Append(name); + remainingBits &= ~fieldKey; + + if (fieldKey == 0) + { + // Do not process further fields if the value equals zero. + Debug.Assert(key == 0); + break; + } } - enumValues[i] = name; } - converted = string.Join(ValueSeparator, enumValues); + if (remainingBits == 0 && sb.Length > 0) + { + // The value is representable as a combination of flags. + stringValue = sb.ToString(); + return true; + } } - return converted; + stringValue = null; + return false; } - private static string[] SplitFlagsEnum(string value) + private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy, out bool containsNameAttributes) { - // todo: optimize implementation here by leveraging https://github.com/dotnet/runtime/issues/934. - return value.Split( + containsNameAttributes = false; #if NET - ValueSeparator + string[] names = Enum.GetNames(); + T[] values = Enum.GetValues(); #else - new string[] { ValueSeparator }, StringSplitOptions.None + 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); + containsNameAttributes = true; + } + } + + var enumFields = new EnumFieldInfo[names.Length]; + for (int i = 0; i < names.Length; i++) + { + string name = names[i]; + T value = values[i]; + ulong key = ConvertToUInt64(value); + bool isAttributeOverriddenName = false; + + if (enumMemberAttributes != null && enumMemberAttributes.TryGetValue(name, out string? attributeName)) + { + name = attributeName; + isAttributeOverriddenName = true; + } + else + { + // Only apply naming policy if there is no JsonStringEnumMemberNameAttribute. + name = ApplyNamingPolicy(name, namingPolicy); + } + + // If enum contains special char, make it failed to serialize or deserialize. + if (name.AsSpan().IndexOfAny(',', ' ') >= 0) + { + ThrowHelper.ThrowInvalidOperationException_InvalidEnumTypeWithSpecialChar(typeof(T), name); + } + + enumFields[i] = new EnumFieldInfo( + name, + originalName: names[i], + value, + key, + isAttributeOverriddenName); + } + + return enumFields; + } + + private sealed class EnumFieldInfo(string name, string originalName, T value, ulong key, bool isAttributeOverriddenName) + { + public string Name { get; } = name; + public string OriginalName { get; } = originalName; + public T Value { get; } = value; + public ulong Key { get; } = key; + public bool IsAttributeOverriddenName { get; } = isAttributeOverriddenName; } } } 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..f16238809c437 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 @@ -6,9 +6,9 @@ namespace System.Text.Json.Serialization.Converters { - [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] internal sealed class EnumConverterFactory : JsonConverterFactory { + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] public EnumConverterFactory() { } @@ -18,23 +18,42 @@ public override bool CanConvert(Type type) return type.IsEnum; } + [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 (Type.GetTypeCode(typeof(T)) is TypeCode.Char) + { + // 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 (Type.GetTypeCode(enumType) is TypeCode.Char) + { + // 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); + object?[] converterParams = [converterOptions, namingPolicy, options]; + return (JsonConverter)Activator.CreateInstance(converterType, converterParams)!; + } } } 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..c90b65933edf2 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumMemberNameAttribute.cs @@ -0,0 +1,31 @@ +// 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) + { + if (name is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(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/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..2001ecc1f7210 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 @@ -92,4 +92,16 @@ let ``Successful Deserialize Numeric label Of Enum When Allowing Integer Values` 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 + Assert.Equal(LanguagePrimitives.EnumOfValue 3, actual) + +type CharEnum = + | A = 'A' + | B = 'B' + | C = 'C' + +[] +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..18ecd23002974 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 @@ -2,8 +2,8 @@ // 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; @@ -832,5 +832,94 @@ 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) + { + JsonSerializerOptions options = new() { Converters = { new JsonStringEnumConverter() } }; + + string json = JsonSerializer.Serialize(value, options); + Assert.Equal($"\"{expectedJson}\"", json); + Assert.Equal(value, JsonSerializer.Deserialize(json, options)); + } + + [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)); + } + + public enum EnumWithMemberAttributes + { + [JsonStringEnumMemberName("CustomValue1")] + Value1 = 1, + [JsonStringEnumMemberName("CustomValue2")] + Value2 = 2, + Value3 = 3, + } + + [Theory] + [InlineData(EnumFlagsWithMemberAttributes.Value1, "A")] + [InlineData(EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2, "A, B")] + [InlineData(EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2 | EnumFlagsWithMemberAttributes.Value3, "A, B, C")] + public static void EnumFlagsWithMemberAttributes_SerializesAsExpected(EnumFlagsWithMemberAttributes value, string expectedJson) + { + string json = JsonSerializer.Serialize(value); + Assert.Equal($"\"{expectedJson}\"", json); + Assert.Equal(value, JsonSerializer.Deserialize(json)); + } + + [Flags, JsonConverter(typeof(JsonStringEnumConverter))] + public enum EnumFlagsWithMemberAttributes + { + [JsonStringEnumMemberName("A")] + Value1 = 1, + [JsonStringEnumMemberName("B")] + Value2 = 2, + [JsonStringEnumMemberName("C")] + Value3 = 4, + } + + [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, + } } } 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; } From 396e9871ca2473234abb4bf6b35241f3359c6398 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 18 Jul 2024 20:26:31 +0100 Subject: [PATCH 02/11] Add testing and refine validation semantics. --- .../src/Resources/Strings.resx | 4 +- .../src/System/ReflectionExtensions.cs | 4 +- .../Converters/Value/EnumConverter.cs | 195 +++++++++--------- .../Converters/Value/EnumConverterFactory.cs | 17 +- .../JsonStringEnumMemberNameAttribute.cs | 5 - .../Text/Json/ThrowHelper.Serialization.cs | 4 +- .../CollectionTests.Dictionary.KeyPolicy.cs | 3 +- .../EnumTests.fs | 20 +- .../Serialization/EnumConverterTests.cs | 149 ++++++++++++- 9 files changed, 275 insertions(+), 126 deletions(-) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index d7b73df8a9e87..3191485550ffd 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}'. '{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/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/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs index c6ba48375ba36..2bfdaab0c685e 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 @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -53,25 +52,27 @@ internal sealed class EnumConverter : JsonPrimitiveConverter // Since multiple threads can add to the cache, a few more values might be added. private const int NameCacheSizeSoftLimit = 64; - public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions serializerOptions) + public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options) { - Debug.Assert(s_enumTypeCode is TypeCode.SByte or TypeCode.Int16 or TypeCode.Int32 or TypeCode.Int64 - or TypeCode.Byte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCode.UInt64); + Debug.Assert(EnumConverterFactory.IsSupportedTypeCode(s_enumTypeCode)); _converterOptions = converterOptions; _namingPolicy = namingPolicy; _enumFieldInfo = ResolveEnumFields(namingPolicy, out _containsNameAttributes); + _nameCacheForWriting = new(); if (namingPolicy != null || _containsNameAttributes) { + // We can't rely on the built-in enum parser since custom names are used. _nameCacheForReading = new(StringComparer.Ordinal); } - JavaScriptEncoder? encoder = serializerOptions.Encoder; + JavaScriptEncoder? encoder = options.Encoder; foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) { - _nameCacheForWriting.TryAdd(fieldInfo.Key, JsonEncodedText.Encode(fieldInfo.Name, encoder)); - _nameCacheForReading?.TryAdd(fieldInfo.Name, fieldInfo.Value); + JsonEncodedText encodedName = JsonEncodedText.Encode(fieldInfo.JsonName, encoder); + _nameCacheForWriting.TryAdd(fieldInfo.Key, encodedName); + _nameCacheForReading?.TryAdd(fieldInfo.JsonName, fieldInfo.Value); } } @@ -79,15 +80,11 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial { JsonTokenType token = reader.TokenType; - if (token == JsonTokenType.String) + if (token is JsonTokenType.String && + (_converterOptions & EnumConverterOptions.AllowStrings) != 0 && + TryParseEnumFromString(ref reader, out T value)) { - if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0 && - TryParseEnumFromString(ref reader, out T value)) - { - return value; - } - - ThrowHelper.ThrowJsonException(); + return value; } if (token != JsonTokenType.Number || (_converterOptions & EnumConverterOptions.AllowNumbers) == 0) @@ -168,10 +165,8 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions return; } - if (TryFormatEnumAsString(value, dictionaryKeyPolicy: null, out string? stringValue, out bool _)) + if (TryFormatEnumAsString(key, value, dictionaryKeyPolicy: null, out string? stringValue)) { - Debug.Assert(s_isFlagsEnum, "path should only be entered by flag enums."); - if (_nameCacheForWriting.Count < NameCacheSizeSoftLimit) { formatted = JsonEncodedText.Encode(stringValue, options.Encoder); @@ -223,13 +218,16 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions writer.WriteNumberValue(Unsafe.As(ref value)); break; default: - ThrowHelper.ThrowJsonException(); + Debug.Fail("Should not be reached"); break; } } internal override T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + // NB JsonSerializerOptions.DictionaryKeyPolicy is ignored on deserialization. + // This is true for all converters that implement dictionary key serialization. + if (!TryParseEnumFromString(ref reader, out T value)) { ThrowHelper.ThrowJsonException(); @@ -240,18 +238,20 @@ internal override T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeT 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 is null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted)) + if (dictionaryKeyPolicy is null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted)) { writer.WritePropertyName(formatted); return; } - if (TryFormatEnumAsString(value, options.DictionaryKeyPolicy, out string? stringEnum, out bool isDictionaryKeyPolicyApplied)) + if (TryFormatEnumAsString(key, value, dictionaryKeyPolicy, out string? stringEnum)) { - if (!isDictionaryKeyPolicyApplied && _nameCacheForWriting.Count < NameCacheSizeSoftLimit) + if (dictionaryKeyPolicy is null && _nameCacheForWriting.Count < NameCacheSizeSoftLimit) { + // Only attempt to cache if there is no dictionary key policy. formatted = JsonEncodedText.Encode(stringEnum, options.Encoder); writer.WritePropertyName(formatted); _nameCacheForWriting.TryAdd(key, formatted); @@ -296,7 +296,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, J writer.WritePropertyName(Unsafe.As(ref value)); break; default: - ThrowHelper.ThrowJsonException(); + Debug.Fail("Should not be reached"); break; } } @@ -344,14 +344,14 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) #if NET9_0_OR_GREATER success = nameCacheForReading.GetAlternateLookup>().TryGetValue(source, out value) || - (source.Contains(',') && TryParseFromCommaSeparatedString(source.ToString(), out value)); + (source.Contains(',') && TryParseCommaSeparatedEnumValues(source.ToString(), out value)); #elif NET string enumString = source.ToString(); success = nameCacheForReading.TryGetValue(enumString, out value) || - TryParseFromCommaSeparatedString(enumString, out value); + TryParseCommaSeparatedEnumValues(enumString, out value); #else success = nameCacheForReading.TryGetValue(source, out value) || - TryParseFromCommaSeparatedString(source, out value); + TryParseCommaSeparatedEnumValues(source, out value); #endif End: @@ -381,7 +381,7 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) JsonArray enumValues = []; foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) { - enumValues.Add((JsonNode)fieldInfo.Name); + enumValues.Add((JsonNode)fieldInfo.JsonName); } return new() { Enum = enumValues }; @@ -390,7 +390,7 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) return new() { Type = JsonSchemaType.Integer }; } - private bool TryParseFromCommaSeparatedString(string enumString, out T value) + private bool TryParseCommaSeparatedEnumValues(string enumString, out T value) { Debug.Assert(_nameCacheForReading != null); @@ -413,7 +413,7 @@ private bool TryParseFromCommaSeparatedString(string enumString, out T value) result |= ConvertToUInt64(value); } - value = (T)Enum.ToObject(typeof(T), result); + value = ConvertFromUInt64(result); if (nameCacheForReading.Count < NameCacheSizeSoftLimit) { @@ -423,34 +423,40 @@ private bool TryParseFromCommaSeparatedString(string enumString, out T value) return true; } - // 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(T value) { - return s_enumTypeCode switch + switch (s_enumTypeCode) { - TypeCode.Int32 or TypeCode.UInt32 => Unsafe.As(ref value), - TypeCode.Int64 or TypeCode.UInt64 => Unsafe.As(ref value), - TypeCode.Int16 or TypeCode.UInt16 => Unsafe.As(ref value), - TypeCode.Byte or TypeCode.SByte => Unsafe.As(ref value), - _ => Throw(), + 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); }; - - static ulong Throw() => throw new InvalidOperationException(); } - private static string ApplyNamingPolicy(string value, JsonNamingPolicy? namingPolicy) + private static T ConvertFromUInt64(ulong value) { - if (namingPolicy is not null) + switch (s_enumTypeCode) { - value = namingPolicy.ConvertName(value); - if (value is null) - { - ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy); - } - } + case TypeCode.Int32 or TypeCode.UInt32: + uint uintValue = (uint)value; + return Unsafe.As(ref uintValue); - return value; + 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); + }; } private static string[] SplitFlagsEnum(string value) @@ -468,36 +474,23 @@ private static string[] SplitFlagsEnum(string value) /// /// Attempt to format the enum value as a comma-separated string of flag values, or returns false if not a valid flag combination. /// - private bool TryFormatEnumAsString( - T value, - JsonNamingPolicy? dictionaryKeyPolicy, - [NotNullWhen(true)] out string? stringValue, - out bool isDictionaryKeyPolicyApplied) + private bool TryFormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictionaryKeyPolicy, [NotNullWhen(true)] out string? stringValue) { - Debug.Assert(!Enum.IsDefined(typeof(T), value) || dictionaryKeyPolicy is not null, "Defined values must have already been handled."); - isDictionaryKeyPolicyApplied = false; + Debug.Assert(!Enum.IsDefined(typeof(T), value) || dictionaryKeyPolicy != null, "Must either be used on undefined values or with a key policy."); - if (s_isFlagsEnum || dictionaryKeyPolicy is not null) + if (s_isFlagsEnum) { - ulong key = ConvertToUInt64(value); - ulong remainingBits = key; using ValueStringBuilder sb = new(stackalloc char[JsonConstants.StackallocCharThreshold]); + ulong remainingBits = key; foreach (EnumFieldInfo enumField in _enumFieldInfo) { ulong fieldKey = enumField.Key; if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey) { - string name; - if (dictionaryKeyPolicy != null && !enumField.IsAttributeOverriddenName) - { - name = ApplyNamingPolicy(enumField.OriginalName, dictionaryKeyPolicy); - isDictionaryKeyPolicyApplied = true; - } - else - { - name = enumField.Name; - } + string name = dictionaryKeyPolicy is not null + ? ResolveAndValidateJsonName(enumField.Name, dictionaryKeyPolicy, enumField.IsNameFromAttribute) + : enumField.JsonName; if (sb.Length > 0) { @@ -518,11 +511,23 @@ private bool TryFormatEnumAsString( if (remainingBits == 0 && sb.Length > 0) { - // The value is representable as a combination of flags. + // The value is a valid combination of flags. stringValue = sb.ToString(); return true; } } + else if (dictionaryKeyPolicy != null) + { + foreach (EnumFieldInfo enumField in _enumFieldInfo) + { + // Search for an exact match and apply the key policy. + if (enumField.Key == key) + { + stringValue = ResolveAndValidateJsonName(enumField.Name, dictionaryKeyPolicy, enumField.IsNameFromAttribute); + return true; + } + } + } stringValue = null; return false; @@ -546,7 +551,6 @@ private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy, if (field.GetCustomAttribute() is { } attribute) { (enumMemberAttributes ??= new(StringComparer.Ordinal)).Add(field.Name, attribute.Name); - containsNameAttributes = true; } } @@ -556,43 +560,46 @@ private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy, string name = names[i]; T value = values[i]; ulong key = ConvertToUInt64(value); - bool isAttributeOverriddenName = false; + bool isNameFromAttribute = false; if (enumMemberAttributes != null && enumMemberAttributes.TryGetValue(name, out string? attributeName)) { name = attributeName; - isAttributeOverriddenName = true; - } - else - { - // Only apply naming policy if there is no JsonStringEnumMemberNameAttribute. - name = ApplyNamingPolicy(name, namingPolicy); - } - - // If enum contains special char, make it failed to serialize or deserialize. - if (name.AsSpan().IndexOfAny(',', ' ') >= 0) - { - ThrowHelper.ThrowInvalidOperationException_InvalidEnumTypeWithSpecialChar(typeof(T), name); + containsNameAttributes = isNameFromAttribute = true; } - enumFields[i] = new EnumFieldInfo( - name, - originalName: names[i], - value, - key, - isAttributeOverriddenName); + string jsonName = ResolveAndValidateJsonName(name, namingPolicy, isNameFromAttribute); + enumFields[i] = new EnumFieldInfo(key, value, name, jsonName, isNameFromAttribute); } return enumFields; } - private sealed class EnumFieldInfo(string name, string originalName, T value, ulong key, bool isAttributeOverriddenName) + private static string ResolveAndValidateJsonName(string name, JsonNamingPolicy? namingPolicy, bool isNameFromAttribute) + { + if (!isNameFromAttribute && namingPolicy is not null) + { + // Do not apply a naming policy to names that are explicitly set via attributes. + // This is consistent with JsonPropertyNameAttribute semantics. + name = namingPolicy.ConvertName(name); + } + + if (name is null || (s_isFlagsEnum && (name is "" || name.AsSpan().IndexOfAny(' ', ',') >= 0))) + { + // Reject null strings and in the case of flags additionally reject empty strings or names containing spaces or commas. + ThrowHelper.ThrowInvalidOperationException_UnsupportedEnumIdentifier(typeof(T), name); + } + + return name; + } + + private sealed class EnumFieldInfo(ulong key, T value, string name, string jsonName, bool isNameFromAttribute) { - public string Name { get; } = name; - public string OriginalName { get; } = originalName; - public T Value { get; } = value; public ulong Key { get; } = key; - public bool IsAttributeOverriddenName { get; } = isAttributeOverriddenName; + public T Value { get; } = value; + public string Name { get; } = name; + public string JsonName { get; } = jsonName; + public bool IsNameFromAttribute { get; } = isNameFromAttribute; } } } 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 f16238809c437..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,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Reflection; namespace System.Text.Json.Serialization.Converters { @@ -18,6 +19,12 @@ 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) @@ -29,7 +36,7 @@ public override JsonConverter CreateConverter(Type type, JsonSerializerOptions o public static JsonConverter Create(EnumConverterOptions converterOptions, JsonSerializerOptions options, JsonNamingPolicy? namingPolicy = null) where T : struct, Enum { - if (Type.GetTypeCode(typeof(T)) is TypeCode.Char) + 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(); @@ -38,22 +45,22 @@ public static JsonConverter Create(EnumConverterOptions converterOptions, 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.")] public static JsonConverter Create(Type enumType, EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options) { - if (Type.GetTypeCode(enumType) is TypeCode.Char) + 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); - object?[] converterParams = [converterOptions, namingPolicy, options]; - return (JsonConverter)Activator.CreateInstance(converterType, converterParams)!; + 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/JsonStringEnumMemberNameAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumMemberNameAttribute.cs index c90b65933edf2..192588e2909b8 100644 --- 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 @@ -15,11 +15,6 @@ public class JsonStringEnumMemberNameAttribute : Attribute /// The name to apply to the current enum member. public JsonStringEnumMemberNameAttribute(string name) { - if (name is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(name)); - } - Name = name; } 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/System.Text.Json.FSharp.Tests/EnumTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/EnumTests.fs index 2001ecc1f7210..c97bb3dbfe5e9 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.Equal("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.Equal("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.Equal("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.Equal("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message) type NumericLabelEnum = | ``1`` = 1 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 18ecd23002974..226bfa97af06f 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,4 +1,4 @@ -// 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; @@ -870,6 +870,32 @@ public static void EnumWithMemberAttributes_StringEnumConverterWithNamingPolicy_ 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); + } + public enum EnumWithMemberAttributes { [JsonStringEnumMemberName("CustomValue1")] @@ -881,8 +907,11 @@ public enum EnumWithMemberAttributes [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, "A, B, C")] + [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); @@ -890,6 +919,34 @@ public static void EnumFlagsWithMemberAttributes_SerializesAsExpected(EnumFlagsW 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); + } + [Flags, JsonConverter(typeof(JsonStringEnumConverter))] public enum EnumFlagsWithMemberAttributes { @@ -899,6 +956,7 @@ public enum EnumFlagsWithMemberAttributes Value2 = 2, [JsonStringEnumMemberName("C")] Value3 = 4, + Value4 = 8, } [Theory] @@ -921,5 +979,92 @@ public enum EnumWithConflictingMemberAttributes Value2 = 2, Value3 = 3, } + + [Theory] + [InlineData(EnumWithValidMemberNames.Value1, "\"Spaces are allowed in non flags enums\"")] + [InlineData(EnumWithValidMemberNames.Value2, "\"Including support for commas, and other punctuation.\"")] + [InlineData(EnumWithValidMemberNames.Value3, "\"Nice \\uD83D\\uDE80\\uD83D\\uDE80\\uD83D\\uDE80\"")] + [InlineData(EnumWithValidMemberNames.Value4, "\"\"")] + [InlineData(EnumWithValidMemberNames.Value5, "\" \"")] + public static void EnumWithValidMemberNameOverrides(EnumWithValidMemberNames value, string expectedJsonString) + { + string json = JsonSerializer.Serialize(value); + Assert.Equal(expectedJsonString, json); + Assert.Equal(value, JsonSerializer.Deserialize(json)); + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum EnumWithValidMemberNames + { + [JsonStringEnumMemberName("Spaces are allowed in non flags enums")] + Value1 = 1, + + [JsonStringEnumMemberName("Including support for commas, and other punctuation.")] + Value2 = 2, + + [JsonStringEnumMemberName("Nice 🚀🚀🚀")] + Value3 = 3, + + [JsonStringEnumMemberName("")] + Value4 = 4, + + [JsonStringEnumMemberName(" ")] + Value5 = 5, + } + + [Theory] + [InlineData(typeof(EnumWithInvalidMemberName1), "")] + [InlineData(typeof(EnumWithInvalidMemberName2), "")] + [InlineData(typeof(EnumWithInvalidMemberName3), " ")] + [InlineData(typeof(EnumWithInvalidMemberName4), "Spaces not allowed in flags enums")] + [InlineData(typeof(EnumWithInvalidMemberName5), "Commas,not,allowed,in,flags,enums")] + public static void EnumWithInvalidMemberName_Throws(Type enumType, string memberName) + { + JsonSerializerOptions options = new() { Converters = { new JsonStringEnumConverter() } }; + + object value = Activator.CreateInstance(enumType); + string expectedExceptionMessage = $"Enum type '{enumType.Name}' uses unsupported identifier '{memberName}'."; + InvalidOperationException ex; + + ex = Assert.Throws(() => JsonSerializer.Serialize(value, enumType, options)); + Assert.Equal(expectedExceptionMessage, ex.Message); + + ex = Assert.Throws(() => JsonSerializer.Deserialize("\"str\"", enumType, options)); + Assert.Equal(expectedExceptionMessage, ex.Message); + } + + public enum EnumWithInvalidMemberName1 + { + [JsonStringEnumMemberName(null!)] + Value + } + + [Flags] + public enum EnumWithInvalidMemberName2 + { + [JsonStringEnumMemberName("")] + Value + } + + [Flags] + public enum EnumWithInvalidMemberName3 + { + [JsonStringEnumMemberName(" ")] + Value + } + + [Flags] + public enum EnumWithInvalidMemberName4 + { + [JsonStringEnumMemberName("Spaces not allowed in flags enums")] + Value + } + + [Flags] + public enum EnumWithInvalidMemberName5 + { + [JsonStringEnumMemberName("Commas,not,allowed,in,flags,enums")] + Value + } } } From 16aa6ddf99217e7a35498dc6a1baef7d674bfb43 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 18 Jul 2024 21:28:32 +0100 Subject: [PATCH 03/11] Switch to using allocation-free enum parsing. --- .../Converters/Value/EnumConverter.cs | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) 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 2bfdaab0c685e..d43511a5bf381 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 @@ -20,8 +20,6 @@ internal sealed class EnumConverter : JsonPrimitiveConverter private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(typeof(T)); 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; @@ -344,7 +342,7 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) #if NET9_0_OR_GREATER success = nameCacheForReading.GetAlternateLookup>().TryGetValue(source, out value) || - (source.Contains(',') && TryParseCommaSeparatedEnumValues(source.ToString(), out value)); + TryParseCommaSeparatedEnumValues(source, out value); #elif NET string enumString = source.ToString(); success = nameCacheForReading.TryGetValue(enumString, out value) || @@ -390,34 +388,63 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) return new() { Type = JsonSchemaType.Integer }; } - private bool TryParseCommaSeparatedEnumValues(string enumString, out T value) + private bool TryParseCommaSeparatedEnumValues( +#if NET9_0_OR_GREATER + ReadOnlySpan source, +#else + string source, +#endif + out T value) { Debug.Assert(_nameCacheForReading != null); - - if (!enumString.Contains(ValueSeparator)) - { - value = default; - return false; - } - ConcurrentDictionary nameCacheForReading = _nameCacheForReading; +#if NET9_0_OR_GREATER + ConcurrentDictionary.AlternateLookup> alternateLookup = nameCacheForReading.GetAlternateLookup>(); + ReadOnlySpan rest = source; +#else + ReadOnlySpan rest = source.AsSpan(); +#endif ulong result = 0; - foreach (string component in SplitFlagsEnum(enumString)) + do { - if (!nameCacheForReading.TryGetValue(component, out value)) + ReadOnlySpan next; + int i = rest.IndexOf(','); + if (i == -1) + { + next = rest; + rest = default; + } + else + { + next = rest.Slice(0, i); + rest = rest.Slice(i + 1); + } + + next = next.Trim(' '); + +#if NET9_0_OR_GREATER + if (!alternateLookup.TryGetValue(next, out value)) +#else + if (!nameCacheForReading.TryGetValue(next.ToString(), out value)) +#endif { return false; } result |= ConvertToUInt64(value); - } + + } while (!rest.IsEmpty); value = ConvertFromUInt64(result); if (nameCacheForReading.Count < NameCacheSizeSoftLimit) { - nameCacheForReading[enumString] = value; +#if NET9_0_OR_GREATER + alternateLookup[source] = value; +#else + nameCacheForReading[source] = value; +#endif } return true; @@ -459,18 +486,6 @@ private static T ConvertFromUInt64(ulong value) }; } - private static string[] SplitFlagsEnum(string value) - { - // 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 - ); - } - /// /// Attempt to format the enum value as a comma-separated string of flag values, or returns false if not a valid flag combination. /// @@ -494,7 +509,7 @@ private bool TryFormatEnumAsString(ulong key, T value, JsonNamingPolicy? diction if (sb.Length > 0) { - sb.Append(ValueSeparator); + sb.Append(", "); } sb.Append(name); From 2bb76c3d52e70c9da2b7400bad996818db502e35 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 18 Jul 2024 21:45:43 +0100 Subject: [PATCH 04/11] Rework build conditional structure. --- .../Converters/Value/EnumConverter.cs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) 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 d43511a5bf381..797844d831052 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 @@ -340,17 +340,7 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) goto End; } -#if NET9_0_OR_GREATER - success = nameCacheForReading.GetAlternateLookup>().TryGetValue(source, out value) || - TryParseCommaSeparatedEnumValues(source, out value); -#elif NET - string enumString = source.ToString(); - success = nameCacheForReading.TryGetValue(enumString, out value) || - TryParseCommaSeparatedEnumValues(enumString, out value); -#else - success = nameCacheForReading.TryGetValue(source, out value) || - TryParseCommaSeparatedEnumValues(source, out value); -#endif + success = TryParseCommaSeparatedEnumValues(source, out value); End: #if NET @@ -389,10 +379,10 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) } private bool TryParseCommaSeparatedEnumValues( -#if NET9_0_OR_GREATER +#if NET ReadOnlySpan source, #else - string source, + string sourceString, #endif out T value) { @@ -400,9 +390,27 @@ private bool TryParseCommaSeparatedEnumValues( ConcurrentDictionary nameCacheForReading = _nameCacheForReading; #if NET9_0_OR_GREATER ConcurrentDictionary.AlternateLookup> alternateLookup = nameCacheForReading.GetAlternateLookup>(); + if (alternateLookup.TryGetValue(source, out value)) + { + return true; + } + + ReadOnlySpan rest = source; +#elif NET + string sourceString = source.ToString(); + if (nameCacheForReading.TryGetValue(sourceString, out value)) + { + return true; + } + ReadOnlySpan rest = source; #else - ReadOnlySpan rest = source.AsSpan(); + if (nameCacheForReading.TryGetValue(sourceString, out value)) + { + return true; + } + + ReadOnlySpan rest = sourceString.AsSpan(); #endif ulong result = 0; @@ -443,7 +451,7 @@ private bool TryParseCommaSeparatedEnumValues( #if NET9_0_OR_GREATER alternateLookup[source] = value; #else - nameCacheForReading[source] = value; + nameCacheForReading[sourceString] = value; #endif } From 0903d4e374b4ad7fcfcce703eeb2e4a071c20d9d Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 19 Jul 2024 07:37:13 +0100 Subject: [PATCH 05/11] Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Cantú --- .../Text/Json/Serialization/Converters/Value/EnumConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 797844d831052..8aa1354500108 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 @@ -333,7 +333,7 @@ private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) Debug.Assert(_nameCacheForReading is null == (_namingPolicy is null && !_containsNameAttributes), "A read cache should only be populated if we have a naming policy or name attributes."); - if (_nameCacheForReading is not { } nameCacheForReading) + if (_nameCacheForReading is null) { value = default; success = false; From 2ee6bf2d8de8b649bf5f96d69d747d8002d37747 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 19 Jul 2024 10:34:28 +0100 Subject: [PATCH 06/11] Improve performance when serializing enum values. --- .../Converters/Value/EnumConverter.cs | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) 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 8aa1354500108..951e909558c00 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 @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Encodings.Web; @@ -163,8 +162,10 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions return; } - if (TryFormatEnumAsString(key, value, dictionaryKeyPolicy: null, out string? stringValue)) + if (IsDefinedValueOrCombinationOfValues(key)) { + 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(stringValue, options.Encoder); @@ -245,12 +246,13 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, J return; } - if (TryFormatEnumAsString(key, value, dictionaryKeyPolicy, out string? stringEnum)) + if (IsDefinedValueOrCombinationOfValues(key)) { + string stringValue = FormatEnumAsString(key, value, dictionaryKeyPolicy); if (dictionaryKeyPolicy is null && _nameCacheForWriting.Count < NameCacheSizeSoftLimit) { // Only attempt to cache if there is no dictionary key policy. - formatted = JsonEncodedText.Encode(stringEnum, options.Encoder); + formatted = JsonEncodedText.Encode(stringValue, options.Encoder); writer.WritePropertyName(formatted); _nameCacheForWriting.TryAdd(key, formatted); } @@ -258,7 +260,7 @@ 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(stringEnum); + writer.WritePropertyName(stringValue); } return; @@ -497,9 +499,12 @@ private static T ConvertFromUInt64(ulong value) /// /// Attempt to format the enum value as a comma-separated string of flag values, or returns false if not a valid flag combination. /// - private bool TryFormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictionaryKeyPolicy, [NotNullWhen(true)] out string? stringValue) + private string FormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictionaryKeyPolicy) { - Debug.Assert(!Enum.IsDefined(typeof(T), value) || dictionaryKeyPolicy != null, "Must either be used on undefined values or with a key policy."); + 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) { @@ -511,6 +516,7 @@ private bool TryFormatEnumAsString(ulong key, T value, JsonNamingPolicy? diction ulong fieldKey = enumField.Key; if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey) { + remainingBits &= ~fieldKey; string name = dictionaryKeyPolicy is not null ? ResolveAndValidateJsonName(enumField.Name, dictionaryKeyPolicy, enumField.IsNameFromAttribute) : enumField.JsonName; @@ -521,38 +527,53 @@ private bool TryFormatEnumAsString(ulong key, T value, JsonNamingPolicy? diction } sb.Append(name); - remainingBits &= ~fieldKey; - if (fieldKey == 0) + if (remainingBits == 0) { - // Do not process further fields if the value equals zero. - Debug.Assert(key == 0); break; } } } - if (remainingBits == 0 && sb.Length > 0) - { - // The value is a valid combination of flags. - stringValue = sb.ToString(); - return true; - } + Debug.Assert(remainingBits == 0 && sb.Length > 0, "unexpected remaining bits or empty string."); + return sb.ToString(); } - else if (dictionaryKeyPolicy != null) + else { + Debug.Assert(dictionaryKeyPolicy != null); + foreach (EnumFieldInfo enumField in _enumFieldInfo) { // Search for an exact match and apply the key policy. if (enumField.Key == key) { - stringValue = ResolveAndValidateJsonName(enumField.Name, dictionaryKeyPolicy, enumField.IsNameFromAttribute); + return ResolveAndValidateJsonName(enumField.Name, dictionaryKeyPolicy, enumField.IsNameFromAttribute); + } + } + + Debug.Fail("should not have been reached."); + return null; + } + } + + private bool IsDefinedValueOrCombinationOfValues(ulong key) + { + 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; } } } - stringValue = null; return false; } From 6ee492116b2696224616a4764dd1da8a561f38c7 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 19 Jul 2024 23:25:12 +0100 Subject: [PATCH 07/11] Rework parsing implementation; refine casing semantics and identifier validation; add more tests. --- .../src/Resources/Strings.resx | 2 +- .../src/System/Text/Json/JsonHelpers.cs | 49 +- .../Converters/Value/EnumConverter.cs | 533 +++++++++--------- .../EnumTests.fs | 35 +- .../Serialization/EnumConverterTests.cs | 162 +++++- 5 files changed, 477 insertions(+), 304 deletions(-) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 3191485550ffd..002fa45a7bdee 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -232,7 +232,7 @@ '{0}' is invalid within a JSON string. The string should be correctly escaped. - Enum type '{0}' uses unsupported identifier '{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/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index 9547edf23b06b..9679c1a68cc1e 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 @@ -563,5 +563,52 @@ static int IndexOfFirstTrailingZero(ReadOnlySpan span) } } } + +#if !NET + // Polyfill for MemoryExtensions.Trim + public static ReadOnlySpan Trim(this ReadOnlySpan source) + { + int start = 0; + int end = source.Length - 1; + + while (start <= end && char.IsWhiteSpace(source[start])) + { + start++; + } + + while (end >= start && char.IsWhiteSpace(source[end])) + { + end--; + } + + return source.Slice(start, end - start + 1); + } + + public static ReadOnlySpan TrimStart(this ReadOnlySpan source) + { + int start = 0; + int end = source.Length - 1; + + while (start <= end && char.IsWhiteSpace(source[start])) + { + start++; + } + + return source.Slice(start, end - start + 1); + } + + public static ReadOnlySpan TrimEnd(this ReadOnlySpan source) + { + int start = 0; + int end = source.Length - 1; + + while (end >= start && char.IsWhiteSpace(source[end])) + { + end--; + } + + return source.Slice(start, end - start + 1); + } +#endif } } 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 951e909558c00..1fbbf86d2a766 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 @@ -17,6 +17,9 @@ internal sealed class EnumConverter : JsonPrimitiveConverter where T : struct, Enum { private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(typeof(T)); + + // Odd type codes are conveniently signed types (for enum backing types). + 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 readonly EnumConverterOptions _converterOptions; @@ -24,26 +27,28 @@ internal sealed class EnumConverter : JsonPrimitiveConverter private readonly JsonNamingPolicy? _namingPolicy; /// - /// Whether either of the enum fields have been overridden with . + /// Stores metadata for the individual fields declared on the enum. /// - private readonly bool _containsNameAttributes; + private readonly EnumFieldInfo[] _enumFieldInfo; /// - /// Stores metadata for the individual fields declared on the enum. + /// 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 EnumFieldInfo[] _enumFieldInfo; + private readonly Dictionary _enumFieldInfoIndex; /// - /// Holds a mapping from enum value to text that might be formatted with . + /// 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. @@ -55,92 +60,67 @@ public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? na _converterOptions = converterOptions; _namingPolicy = namingPolicy; - _enumFieldInfo = ResolveEnumFields(namingPolicy, out _containsNameAttributes); + _enumFieldInfo = ResolveEnumFields(namingPolicy); _nameCacheForWriting = new(); - if (namingPolicy != null || _containsNameAttributes) - { - // We can't rely on the built-in enum parser since custom names are used. - _nameCacheForReading = new(StringComparer.Ordinal); - } + _nameCacheForReading = new(StringComparer.Ordinal); + _enumFieldInfoIndex = new(StringComparer.OrdinalIgnoreCase); JavaScriptEncoder? encoder = options.Encoder; foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) { JsonEncodedText encodedName = JsonEncodedText.Encode(fieldInfo.JsonName, encoder); _nameCacheForWriting.TryAdd(fieldInfo.Key, encodedName); - _nameCacheForReading?.TryAdd(fieldInfo.JsonName, fieldInfo.Value); + _nameCacheForReading.TryAdd(fieldInfo.JsonName, fieldInfo.Key); + AddToEnumFieldIndex(fieldInfo); } - } - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - JsonTokenType token = reader.TokenType; - - if (token is JsonTokenType.String && - (_converterOptions & EnumConverterOptions.AllowStrings) != 0 && - TryParseEnumFromString(ref reader, out T value)) + if (namingPolicy != null) { - return value; + // 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) + { + if (fieldInfo.Kind is EnumFieldNameKind.NamingPolicy) + { + AddToEnumFieldIndex(new EnumFieldInfo(fieldInfo.Key, EnumFieldNameKind.Default, fieldInfo.OriginalName, fieldInfo.OriginalName)); + } + } } - if (token != JsonTokenType.Number || (_converterOptions & EnumConverterOptions.AllowNumbers) == 0) + void AddToEnumFieldIndex(EnumFieldInfo fieldInfo) { - ThrowHelper.ThrowJsonException(); + 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)) + case JsonTokenType.String when (_converterOptions & EnumConverterOptions.AllowStrings) != 0: + if (TryParseEnumFromString(ref reader, out T result)) { - // Use Unsafe.As instead of raw pointers for .NET Standard support. - // https://github.com/dotnet/runtime/issues/84895 - return Unsafe.As(ref int32); + return result; } 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)) - { - return Unsafe.As(ref int16); - } - 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; } @@ -151,8 +131,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); @@ -183,42 +163,18 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions } } - 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: - Debug.Fail("Should not be reached"); - break; + writer.WriteNumberValue(ConvertToInt64(value)); + } + else + { + writer.WriteNumberValue(ConvertToUInt64(value)); } } @@ -227,12 +183,12 @@ internal override T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeT // NB JsonSerializerOptions.DictionaryKeyPolicy is ignored on deserialization. // This is true for all converters that implement dictionary key serialization. - if (!TryParseEnumFromString(ref reader, out T value)) + if (!TryParseEnumFromString(ref reader, out T result)) { ThrowHelper.ThrowJsonException(); } - return value; + return result; } internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, JsonSerializerOptions options, bool isWritingExtensionDataProperty) @@ -248,6 +204,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, J if (IsDefinedValueOrCombinationOfValues(key)) { + 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) { @@ -266,155 +223,93 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, J 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: - Debug.Fail("Should not be reached"); - break; + writer.WritePropertyName(ConvertToInt64(value)); + } + else + { + writer.WritePropertyName(key); } } - private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T value) + private bool TryParseEnumFromString(ref Utf8JsonReader reader, out T result) { - bool success; -#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); - Span 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 = reader.GetString(); + string source = ((ReadOnlySpan)charBuffer).Trim().ToString(); + ConcurrentDictionary lookup = _nameCacheForReading; #endif - // Skip the built-in enum parser and go directly to the read cache if either: - // - // 1. one of the enum fields have had their names overridden OR - // 2. the source string represents a number when numbers are not permitted. - // - // For backward compatibility reasons the built-in parser is not skipped if a naming policy is specified. - bool skipEnumParser = _containsNameAttributes || - ((_converterOptions & EnumConverterOptions.AllowNumbers) is 0 && JsonHelpers.IntegerRegex.IsMatch(source)); - - if (!skipEnumParser && Enum.TryParse(source, ignoreCase: true, out value)) + if (lookup.TryGetValue(source, out ulong key)) { + result = ConvertFromUInt64(key); success = true; goto End; } - Debug.Assert(_nameCacheForReading is null == (_namingPolicy is null && !_containsNameAttributes), - "A read cache should only be populated if we have a naming policy or name attributes."); - - if (_nameCacheForReading is null) + if (JsonHelpers.IntegerRegex.IsMatch(source)) { - value = default; - success = false; - goto End; + // 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 = TryParseNamedEnum(source, out result); } - success = TryParseCommaSeparatedEnumValues(source, out value); + if (success && _nameCacheForReading.Count < NameCacheSizeSoftLimit) + { + lookup.TryAdd(source, ConvertToUInt64(result)); + } End: -#if NET if (rentedBuffer != null) { - source.Clear(); + charBuffer.Clear(); ArrayPool.Shared.Return(rentedBuffer); } -#endif - return success; - } - - internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) - { - 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 }; + return success; } - private bool TryParseCommaSeparatedEnumValues( -#if NET + private bool TryParseNamedEnum( +#if NET9_0_OR_GREATER ReadOnlySpan source, #else - string sourceString, + string source, #endif - out T value) + out T result) { - Debug.Assert(_nameCacheForReading != null); - ConcurrentDictionary nameCacheForReading = _nameCacheForReading; #if NET9_0_OR_GREATER - ConcurrentDictionary.AlternateLookup> alternateLookup = nameCacheForReading.GetAlternateLookup>(); - if (alternateLookup.TryGetValue(source, out value)) - { - return true; - } - - ReadOnlySpan rest = source; -#elif NET - string sourceString = source.ToString(); - if (nameCacheForReading.TryGetValue(sourceString, out value)) - { - return true; - } - + Dictionary.AlternateLookup> lookup = _enumFieldInfoIndex.GetAlternateLookup>(); ReadOnlySpan rest = source; #else - if (nameCacheForReading.TryGetValue(sourceString, out value)) - { - return true; - } - - ReadOnlySpan rest = sourceString.AsSpan(); + Dictionary lookup = _enumFieldInfoIndex; + ReadOnlySpan rest = source.AsSpan(); #endif - ulong result = 0; + ulong key = 0; do { @@ -427,36 +322,29 @@ private bool TryParseCommaSeparatedEnumValues( } else { - next = rest.Slice(0, i); - rest = rest.Slice(i + 1); + next = rest.Slice(0, i).TrimEnd(); + rest = rest.Slice(i + 1).TrimStart(); } - next = next.Trim(' '); - + if (lookup.TryGetValue( #if NET9_0_OR_GREATER - if (!alternateLookup.TryGetValue(next, out value)) + next, #else - if (!nameCacheForReading.TryGetValue(next.ToString(), out value)) + next.ToString(), #endif + out EnumFieldInfo? firstResult) && + firstResult.GetMatchingField(next) is EnumFieldInfo match) { - return false; + key |= match.Key; + continue; } - result |= ConvertToUInt64(value); + result = default; + return false; } while (!rest.IsEmpty); - value = ConvertFromUInt64(result); - - if (nameCacheForReading.Count < NameCacheSizeSoftLimit) - { -#if NET9_0_OR_GREATER - alternateLookup[source] = value; -#else - nameCacheForReading[sourceString] = value; -#endif - } - + result = ConvertFromUInt64(key); return true; } @@ -473,6 +361,20 @@ private static ulong ConvertToUInt64(T value) }; } + private static long ConvertToInt64(T value) + { + Debug.Assert(s_isSignedEnum); + switch (s_enumTypeCode) + { + 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); + }; + } + private static T ConvertFromUInt64(ulong value) { switch (s_enumTypeCode) @@ -518,7 +420,7 @@ private string FormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictiona { remainingBits &= ~fieldKey; string name = dictionaryKeyPolicy is not null - ? ResolveAndValidateJsonName(enumField.Name, dictionaryKeyPolicy, enumField.IsNameFromAttribute) + ? ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind) : enumField.JsonName; if (sb.Length > 0) @@ -547,7 +449,7 @@ private string FormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictiona // Search for an exact match and apply the key policy. if (enumField.Key == key) { - return ResolveAndValidateJsonName(enumField.Name, dictionaryKeyPolicy, enumField.IsNameFromAttribute); + return ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind); } } @@ -558,28 +460,67 @@ private string FormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictiona private bool IsDefinedValueOrCombinationOfValues(ulong key) { - ulong remainingBits = key; - - foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) + if (s_isFlagsEnum) { - ulong fieldKey = fieldInfo.Key; - if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey) + ulong remainingBits = key; + + foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) { - remainingBits &= ~fieldKey; + ulong fieldKey = fieldInfo.Key; + if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey) + { + remainingBits &= ~fieldKey; - if (remainingBits == 0) + if (remainingBits == 0) + { + return true; + } + } + } + + return false; + } + else + { + foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) + { + if (fieldInfo.Key == key) { return true; } } + + return false; + } + } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) + { + 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 false; + return new() { Type = JsonSchemaType.Integer }; } - private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy, out bool containsNameAttributes) + private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy) { - containsNameAttributes = false; #if NET string[] names = Enum.GetNames(); T[] values = Enum.GetValues(); @@ -601,49 +542,115 @@ private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy, var enumFields = new EnumFieldInfo[names.Length]; for (int i = 0; i < names.Length; i++) { - string name = names[i]; + string originalName = names[i]; T value = values[i]; ulong key = ConvertToUInt64(value); - bool isNameFromAttribute = false; + EnumFieldNameKind kind; - if (enumMemberAttributes != null && enumMemberAttributes.TryGetValue(name, out string? attributeName)) + if (enumMemberAttributes != null && enumMemberAttributes.TryGetValue(originalName, out string? attributeName)) { - name = attributeName; - containsNameAttributes = isNameFromAttribute = true; + originalName = attributeName; + kind = EnumFieldNameKind.Attribute; + } + else + { + kind = namingPolicy != null ? EnumFieldNameKind.NamingPolicy : EnumFieldNameKind.Default; } - string jsonName = ResolveAndValidateJsonName(name, namingPolicy, isNameFromAttribute); - enumFields[i] = new EnumFieldInfo(key, value, name, jsonName, isNameFromAttribute); + string jsonName = ResolveAndValidateJsonName(originalName, namingPolicy, kind); + enumFields[i] = new EnumFieldInfo(key, kind, originalName, jsonName); } return enumFields; } - private static string ResolveAndValidateJsonName(string name, JsonNamingPolicy? namingPolicy, bool isNameFromAttribute) + private static string ResolveAndValidateJsonName(string name, JsonNamingPolicy? namingPolicy, EnumFieldNameKind kind) { - if (!isNameFromAttribute && namingPolicy is not null) + if (kind is not EnumFieldNameKind.Attribute && namingPolicy is not null) { // Do not apply a naming policy to names that are explicitly set via attributes. // This is consistent with JsonPropertyNameAttribute semantics. name = namingPolicy.ConvertName(name); } - if (name is null || (s_isFlagsEnum && (name is "" || name.AsSpan().IndexOfAny(' ', ',') >= 0))) + if (string.IsNullOrEmpty(name) || char.IsWhiteSpace(name[0]) || char.IsWhiteSpace(name[name.Length - 1]) || + (s_isFlagsEnum && name.AsSpan().IndexOf(',') >= 0)) { - // Reject null strings and in the case of flags additionally reject empty strings or names containing spaces or commas. + // 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, T value, string name, string jsonName, bool isNameFromAttribute) + 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 T Value { get; } = value; - public string Name { get; } = name; + public string OriginalName { get; } = originalName; public string JsonName { get; } = jsonName; - public bool IsNameFromAttribute { get; } = isNameFromAttribute; + + /// + /// 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) + { + 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)) + { + // 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); + } + + public EnumFieldInfo? GetMatchingField(ReadOnlySpan input) + { + 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; + } + + if (_conflictingFields is { } conflictingFields) + { + Debug.Assert(conflictingFields.Count > 0); + foreach (EnumFieldInfo matchingField in conflictingFields) + { + if (matchingField.Kind is EnumFieldNameKind.Default || input.SequenceEqual(matchingField.JsonName.AsSpan())) + { + return matchingField; + } + } + } + + return null; + } + } + + private enum EnumFieldNameKind + { + Default = 0, + NamingPolicy = 1, + Attribute = 2, } } } 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 c97bb3dbfe5e9..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 @@ -35,13 +35,13 @@ 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("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message) + 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("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message) + Assert.Contains("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message) [] let ``Successful Deserialize Normal Enum`` () = @@ -51,12 +51,12 @@ 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("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message) + 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("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message) + Assert.Contains("Enum type 'BadEnum' uses unsupported identifier 'There's a comma, in my name'.", ex.Message) type NumericLabelEnum = | ``1`` = 1 @@ -64,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) [] [] @@ -84,11 +89,15 @@ 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`` () = - let actual = JsonSerializer.Deserialize("\"3\"", options) - Assert.NotEqual(NumericLabelEnum.``3``, actual) - Assert.Equal(LanguagePrimitives.EnumOfValue 3, 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' 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 226bfa97af06f..09f118a00048b 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 @@ -15,6 +15,9 @@ 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))] @@ -839,11 +842,9 @@ private static JsonSerializerOptions CreateStringEnumOptionsForType(bool [InlineData(EnumWithMemberAttributes.Value3, "Value3")] public static void EnumWithMemberAttributes_StringEnumConverter_SerializesAsExpected(EnumWithMemberAttributes value, string expectedJson) { - JsonSerializerOptions options = new() { Converters = { new JsonStringEnumConverter() } }; - - string json = JsonSerializer.Serialize(value, options); + string json = JsonSerializer.Serialize(value, s_optionsWithStringEnumConverter); Assert.Equal($"\"{expectedJson}\"", json); - Assert.Equal(value, JsonSerializer.Deserialize(json, options)); + Assert.Equal(value, JsonSerializer.Deserialize(json, s_optionsWithStringEnumConverter)); } [Theory] @@ -896,6 +897,28 @@ public static void EnumWithMemberAttributes_NamingPolicyAndDictionaryKeyPolicy_N """, 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")] @@ -947,6 +970,30 @@ public static void EnumFlagsWithMemberAttributes_NamingPolicyAndDictionaryKeyPol """, 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 { @@ -957,6 +1004,7 @@ public enum EnumFlagsWithMemberAttributes [JsonStringEnumMemberName("C")] Value3 = 4, Value4 = 8, + Value5 = 16, } [Theory] @@ -981,11 +1029,11 @@ public enum EnumWithConflictingMemberAttributes } [Theory] - [InlineData(EnumWithValidMemberNames.Value1, "\"Spaces are allowed in non flags enums\"")] + [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, "\"\"")] - [InlineData(EnumWithValidMemberNames.Value5, "\" \"")] + [InlineData(EnumWithValidMemberNames.Value4, "\"5\"")] + [InlineData(EnumWithValidMemberNames.Value1 | EnumWithValidMemberNames.Value4, "5")] public static void EnumWithValidMemberNameOverrides(EnumWithValidMemberNames value, string expectedJsonString) { string json = JsonSerializer.Serialize(value); @@ -993,10 +1041,23 @@ public static void EnumWithValidMemberNameOverrides(EnumWithValidMemberNames val 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("Spaces are allowed in non flags enums")] + [JsonStringEnumMemberName("Intermediate whitespace\t is allowed\r\nin enums")] Value1 = 1, [JsonStringEnumMemberName("Including support for commas, and other punctuation.")] @@ -1005,32 +1066,78 @@ public enum EnumWithValidMemberNames [JsonStringEnumMemberName("Nice 🚀🚀🚀")] Value3 = 3, - [JsonStringEnumMemberName("")] - Value4 = 4, + [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)); + } - [JsonStringEnumMemberName(" ")] - Value5 = 5, + [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), "Spaces not allowed in flags enums")] - [InlineData(typeof(EnumWithInvalidMemberName5), "Commas,not,allowed,in,flags,enums")] + [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) { - JsonSerializerOptions options = new() { Converters = { new JsonStringEnumConverter() } }; - object value = Activator.CreateInstance(enumType); string expectedExceptionMessage = $"Enum type '{enumType.Name}' uses unsupported identifier '{memberName}'."; InvalidOperationException ex; - ex = Assert.Throws(() => JsonSerializer.Serialize(value, enumType, options)); - Assert.Equal(expectedExceptionMessage, ex.Message); + ex = Assert.Throws(() => JsonSerializer.Serialize(value, enumType, s_optionsWithStringEnumConverter)); + Assert.Contains(expectedExceptionMessage, ex.Message); - ex = Assert.Throws(() => JsonSerializer.Deserialize("\"str\"", enumType, options)); - Assert.Equal(expectedExceptionMessage, ex.Message); + ex = Assert.Throws(() => JsonSerializer.Deserialize("\"str\"", enumType, s_optionsWithStringEnumConverter)); + Assert.Contains(expectedExceptionMessage, ex.Message); } public enum EnumWithInvalidMemberName1 @@ -1039,31 +1146,34 @@ public enum EnumWithInvalidMemberName1 Value } - [Flags] public enum EnumWithInvalidMemberName2 { [JsonStringEnumMemberName("")] Value } - [Flags] public enum EnumWithInvalidMemberName3 { [JsonStringEnumMemberName(" ")] Value } - [Flags] public enum EnumWithInvalidMemberName4 { - [JsonStringEnumMemberName("Spaces not allowed in flags enums")] + [JsonStringEnumMemberName(" HasLeadingWhitespace")] Value } - [Flags] public enum EnumWithInvalidMemberName5 { - [JsonStringEnumMemberName("Commas,not,allowed,in,flags,enums")] + [JsonStringEnumMemberName("HasTrailingWhitespace\n")] + Value + } + + [Flags] + public enum EnumWithInvalidMemberName6 + { + [JsonStringEnumMemberName("Comma separators not allowed, in flags enums")] Value } } From ecf01c8753c0850572a13dfeac8ac04b4600f81a Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 19 Jul 2024 23:40:40 +0100 Subject: [PATCH 08/11] Minor change --- .../System.Text.Json/System.Text.Json.sln | 56 +++++++++++-------- .../Converters/Value/EnumConverter.cs | 5 +- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index 11d7c5d4944b0..1c46ef7c2a325 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35103.136 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" @@ -45,7 +49,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\Sys EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn3.11", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn3.11.csproj", "{5C0CE30B-DD4A-4F7A-87C0-5243F0C86885}" EndProject @@ -81,11 +85,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E49881A9-09F EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{F254F143-4704-4432-9995-20D87FA8BF8D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E9E19E81-4AE0-412D-98EA-3B14694D3CA3}" EndProject @@ -249,47 +253,51 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {102945CA-3736-4B2C-8E68-242A0B247F2B} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {73D5739C-E382-4E22-A7D3-B82705C58C74} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {282400DF-F3D8-4419-90F1-1E2F2D8B760C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {09F77672-101E-4495-9D88-29376919C121} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {BE27618A-2916-4269-9AD5-6BC5EDC32B30} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {C2C7BA09-F9EE-4E43-8EE4-871CC000342C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {4774F56D-16A8-4ABB-8C73-5F57609F1773} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {B6D364E7-E5DB-4CF4-B87F-3CEDA3FF7478} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {FAB4FFF2-964D-45FF-89CC-8BB9CE618ED1} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {C56337BB-8CBC-4EE5-AB4D-8BB0A922813E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {B6D364E7-E5DB-4CF4-B87F-3CEDA3FF7478} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {715327DF-D6D6-4043-AC78-FA58F0C0E9E3} = {F254F143-4704-4432-9995-20D87FA8BF8D} {B815304D-502E-402C-ACE1-878DB4985CCC} = {F254F143-4704-4432-9995-20D87FA8BF8D} {E4B72517-C694-486A-950E-6AB03C651FDC} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {FAB4FFF2-964D-45FF-89CC-8BB9CE618ED1} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {C56337BB-8CBC-4EE5-AB4D-8BB0A922813E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {04AEB008-EE4F-44DE-A361-2DBD2D0FD6A4} = {F254F143-4704-4432-9995-20D87FA8BF8D} {6485EED4-C313-4551-9865-8ADCED603629} = {F254F143-4704-4432-9995-20D87FA8BF8D} {143AFE8A-3490-444C-A82D-6A375EB59F01} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {B32C4C93-6C85-4E88-B966-5A9F5CAD54B1} = {F254F143-4704-4432-9995-20D87FA8BF8D} {03D5F977-D2DA-4586-A662-C99B0E0EEC20} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} {A3FCA745-7F4D-4FB8-85A5-422AC92B2704} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} - {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {D0277072-F248-47EF-A417-D2612987D899} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} {A86642C4-E388-4871-B9FB-A5FCB8BF5CB0} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} - {8F44175A-3081-4CA8-90E7-9EE6754EACAA} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {8E1C465B-1DA9-4C2A-A6B5-082C55EFB95E} = {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} + {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} + {8F44175A-3081-4CA8-90E7-9EE6754EACAA} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5868B757-D821-41FC-952E-2113A0519506} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a3fca745-7f4d-4fb8-85a5-422ac92b2704}*SharedItemsImports = 5 + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a86642c4-e388-4871-b9fb-a5fcb8bf5cb0}*SharedItemsImports = 5 + EndGlobalSection EndGlobal 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 1fbbf86d2a766..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 @@ -61,18 +61,19 @@ public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? na _converterOptions = converterOptions; _namingPolicy = namingPolicy; _enumFieldInfo = ResolveEnumFields(namingPolicy); + _enumFieldInfoIndex = new(StringComparer.OrdinalIgnoreCase); _nameCacheForWriting = new(); _nameCacheForReading = new(StringComparer.Ordinal); - _enumFieldInfoIndex = new(StringComparer.OrdinalIgnoreCase); JavaScriptEncoder? encoder = options.Encoder; foreach (EnumFieldInfo fieldInfo in _enumFieldInfo) { + AddToEnumFieldIndex(fieldInfo); + JsonEncodedText encodedName = JsonEncodedText.Encode(fieldInfo.JsonName, encoder); _nameCacheForWriting.TryAdd(fieldInfo.Key, encodedName); _nameCacheForReading.TryAdd(fieldInfo.JsonName, fieldInfo.Key); - AddToEnumFieldIndex(fieldInfo); } if (namingPolicy != null) From bb6349622c892f36721b965877d1ce2bb39b1433 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 22 Jul 2024 11:34:52 +0100 Subject: [PATCH 09/11] Revert .sln changes --- .../System.Text.Json/System.Text.Json.sln | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index 1c46ef7c2a325..11d7c5d4944b0 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -1,8 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35103.136 -MinimumVisualStudioVersion = 10.0.40219.1 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" @@ -49,7 +45,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\Sys EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn3.11", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn3.11.csproj", "{5C0CE30B-DD4A-4F7A-87C0-5243F0C86885}" EndProject @@ -85,11 +81,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E49881A9-09F EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{F254F143-4704-4432-9995-20D87FA8BF8D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E9E19E81-4AE0-412D-98EA-3B14694D3CA3}" EndProject @@ -253,51 +249,47 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {102945CA-3736-4B2C-8E68-242A0B247F2B} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {73D5739C-E382-4E22-A7D3-B82705C58C74} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {282400DF-F3D8-4419-90F1-1E2F2D8B760C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {09F77672-101E-4495-9D88-29376919C121} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {BE27618A-2916-4269-9AD5-6BC5EDC32B30} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {C2C7BA09-F9EE-4E43-8EE4-871CC000342C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {4774F56D-16A8-4ABB-8C73-5F57609F1773} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {B6D364E7-E5DB-4CF4-B87F-3CEDA3FF7478} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {715327DF-D6D6-4043-AC78-FA58F0C0E9E3} = {F254F143-4704-4432-9995-20D87FA8BF8D} - {B815304D-502E-402C-ACE1-878DB4985CCC} = {F254F143-4704-4432-9995-20D87FA8BF8D} - {E4B72517-C694-486A-950E-6AB03C651FDC} = {F254F143-4704-4432-9995-20D87FA8BF8D} {FAB4FFF2-964D-45FF-89CC-8BB9CE618ED1} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {C56337BB-8CBC-4EE5-AB4D-8BB0A922813E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {715327DF-D6D6-4043-AC78-FA58F0C0E9E3} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {B815304D-502E-402C-ACE1-878DB4985CCC} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {E4B72517-C694-486A-950E-6AB03C651FDC} = {F254F143-4704-4432-9995-20D87FA8BF8D} {04AEB008-EE4F-44DE-A361-2DBD2D0FD6A4} = {F254F143-4704-4432-9995-20D87FA8BF8D} {6485EED4-C313-4551-9865-8ADCED603629} = {F254F143-4704-4432-9995-20D87FA8BF8D} {143AFE8A-3490-444C-A82D-6A375EB59F01} = {F254F143-4704-4432-9995-20D87FA8BF8D} - {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {B32C4C93-6C85-4E88-B966-5A9F5CAD54B1} = {F254F143-4704-4432-9995-20D87FA8BF8D} {03D5F977-D2DA-4586-A662-C99B0E0EEC20} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} {A3FCA745-7F4D-4FB8-85A5-422AC92B2704} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} + {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {D0277072-F248-47EF-A417-D2612987D899} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} {A86642C4-E388-4871-B9FB-A5FCB8BF5CB0} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} - {8E1C465B-1DA9-4C2A-A6B5-082C55EFB95E} = {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} - {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {8F44175A-3081-4CA8-90E7-9EE6754EACAA} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} + {8E1C465B-1DA9-4C2A-A6B5-082C55EFB95E} = {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5868B757-D821-41FC-952E-2113A0519506} EndGlobalSection - GlobalSection(SharedMSBuildProjectFiles) = preSolution - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a3fca745-7f4d-4fb8-85a5-422ac92b2704}*SharedItemsImports = 5 - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a86642c4-e388-4871-b9fb-a5fcb8bf5cb0}*SharedItemsImports = 5 - EndGlobalSection EndGlobal From 01875eb64ca43391fcd85ceae351959220192df3 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 22 Jul 2024 12:21:15 +0100 Subject: [PATCH 10/11] Additional naming conflict tests --- .../Serialization/EnumConverterTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 09f118a00048b..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 @@ -9,6 +9,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; +using Newtonsoft.Json; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -1028,6 +1029,36 @@ public enum EnumWithConflictingMemberAttributes 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.\"")] From b3c9bda3fd914830efdf7bddd1e5c6d6cb2b1a41 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 22 Jul 2024 15:08:58 +0100 Subject: [PATCH 11/11] Address feedback. --- .../src/System/Text/Json/JsonHelpers.cs | 47 ------------------- 1 file changed, 47 deletions(-) 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 d0b79389ad78b..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 @@ -570,52 +570,5 @@ static int IndexOfFirstTrailingZero(ReadOnlySpan span) } } } - -#if !NET - // Polyfill for MemoryExtensions.Trim - public static ReadOnlySpan Trim(this ReadOnlySpan source) - { - int start = 0; - int end = source.Length - 1; - - while (start <= end && char.IsWhiteSpace(source[start])) - { - start++; - } - - while (end >= start && char.IsWhiteSpace(source[end])) - { - end--; - } - - return source.Slice(start, end - start + 1); - } - - public static ReadOnlySpan TrimStart(this ReadOnlySpan source) - { - int start = 0; - int end = source.Length - 1; - - while (start <= end && char.IsWhiteSpace(source[start])) - { - start++; - } - - return source.Slice(start, end - start + 1); - } - - public static ReadOnlySpan TrimEnd(this ReadOnlySpan source) - { - int start = 0; - int end = source.Length - 1; - - while (end >= start && char.IsWhiteSpace(source[end])) - { - end--; - } - - return source.Slice(start, end - start + 1); - } -#endif } }