< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 443
Coverable lines: 443
Total lines: 742
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 239
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Collections.Concurrent;
 6using System.Collections.Generic;
 7using System.Diagnostics;
 8using System.Reflection;
 9using System.Runtime.CompilerServices;
 10using System.Text.Encodings.Web;
 11using System.Text.Json.Nodes;
 12using System.Text.Json.Schema;
 13
 14namespace System.Text.Json.Serialization.Converters
 15{
 16    internal sealed class EnumConverter<T> : JsonPrimitiveConverter<T> // Do not rename FQN (legacy schema generation)
 17        where T : struct, Enum
 18    {
 019        private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(typeof(T));
 20
 21        // Odd type codes are conveniently signed types (for enum backing types).
 022        private static readonly bool s_isSignedEnum = ((int)s_enumTypeCode % 2) == 1;
 023        private static readonly bool s_isFlagsEnum = typeof(T).IsDefined(typeof(FlagsAttribute), inherit: false);
 24
 25        private readonly EnumConverterOptions _converterOptions; // Do not rename (legacy schema generation)
 26        private readonly JsonNamingPolicy? _namingPolicy; // Do not rename (legacy schema generation)
 27
 28        /// <summary>
 29        /// Stores metadata for the individual fields declared on the enum.
 30        /// </summary>
 31        private readonly EnumFieldInfo[] _enumFieldInfo;
 32
 33        /// <summary>
 34        /// Defines a case-insensitive index of enum field names to their metadata.
 35        /// In case of casing conflicts, extra fields are appended to a list in the value.
 36        /// This is the main dictionary that is queried by the enum parser implementation.
 37        /// </summary>
 38        private readonly Dictionary<string, EnumFieldInfo> _enumFieldInfoIndex;
 39
 40        /// <summary>
 41        /// Holds a cache from enum value to formatted UTF-8 text including flag combinations.
 42        /// <see cref="ulong"/> is as the key used rather than <typeparamref name="T"/> given measurements that
 43        /// show private memory savings when a single type is used https://github.com/dotnet/runtime/pull/36726#discussi
 44        /// </summary>
 45        private readonly ConcurrentDictionary<ulong, JsonEncodedText> _nameCacheForWriting;
 46
 47        /// <summary>
 48        /// Holds a mapping from input text to enum values including flag combinations and alternative casings.
 49        /// </summary>
 50        private readonly ConcurrentDictionary<string, ulong> _nameCacheForReading;
 51
 52        // This is used to prevent flooding the cache due to exponential bitwise combinations of flags.
 53        // Since multiple threads can add to the cache, a few more values might be added.
 54        private const int NameCacheSizeSoftLimit = 64;
 55
 056        public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOption
 057        {
 058            Debug.Assert(EnumConverterFactory.Helpers.IsSupportedTypeCode(s_enumTypeCode));
 59
 060            _converterOptions = converterOptions;
 061            _namingPolicy = namingPolicy;
 062            _enumFieldInfo = ResolveEnumFields(namingPolicy);
 063            _enumFieldInfoIndex = new(StringComparer.OrdinalIgnoreCase);
 64
 065            _nameCacheForWriting = new();
 066            _nameCacheForReading = new(StringComparer.Ordinal);
 67
 068            JavaScriptEncoder? encoder = options.Encoder;
 069            foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 070            {
 071                AddToEnumFieldIndex(fieldInfo);
 72
 073                JsonEncodedText encodedName = JsonEncodedText.Encode(fieldInfo.JsonName, encoder);
 074                _nameCacheForWriting.TryAdd(fieldInfo.Key, encodedName);
 075                _nameCacheForReading.TryAdd(fieldInfo.JsonName, fieldInfo.Key);
 076            }
 77
 078            if (namingPolicy is not null)
 079            {
 80                // Additionally populate the field index with the default names of fields that used a naming policy.
 81                // This is done to preserve backward compat: default names should still be recognized by the parser.
 082                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 083                {
 084                    if (fieldInfo.Kind is EnumFieldNameKind.NamingPolicy)
 085                    {
 086                        AddToEnumFieldIndex(new EnumFieldInfo(fieldInfo.Key, EnumFieldNameKind.Default, fieldInfo.Origin
 087                    }
 088                }
 089            }
 90
 91            void AddToEnumFieldIndex(EnumFieldInfo fieldInfo)
 092            {
 093                if (!_enumFieldInfoIndex.TryAdd(fieldInfo.JsonName, fieldInfo))
 094                {
 95                    // We have a casing conflict, append field to the existing entry.
 096                    EnumFieldInfo existingFieldInfo = _enumFieldInfoIndex[fieldInfo.JsonName];
 097                    existingFieldInfo.AppendConflictingField(fieldInfo);
 098                }
 099            }
 0100        }
 101
 102        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 0103        {
 0104            switch (reader.TokenType)
 105            {
 0106                case JsonTokenType.String when (_converterOptions & EnumConverterOptions.AllowStrings) != 0:
 0107                    if (TryParseEnumFromString(ref reader, out T result))
 0108                    {
 0109                        return result;
 110                    }
 0111                    break;
 112
 0113                case JsonTokenType.Number when (_converterOptions & EnumConverterOptions.AllowNumbers) != 0:
 0114                    switch (s_enumTypeCode)
 115                    {
 0116                        case TypeCode.Int32 when reader.TryGetInt32(out int int32): return (T)(object)int32;
 0117                        case TypeCode.UInt32 when reader.TryGetUInt32(out uint uint32): return (T)(object)uint32;
 0118                        case TypeCode.Int64 when reader.TryGetInt64(out long int64): return (T)(object)int64;
 0119                        case TypeCode.UInt64 when reader.TryGetUInt64(out ulong uint64): return (T)(object)uint64;
 0120                        case TypeCode.Byte when reader.TryGetByte(out byte ubyte8): return (T)(object)ubyte8;
 0121                        case TypeCode.SByte when reader.TryGetSByte(out sbyte byte8): return (T)(object)byte8;
 0122                        case TypeCode.Int16 when reader.TryGetInt16(out short int16): return (T)(object)int16;
 0123                        case TypeCode.UInt16 when reader.TryGetUInt16(out ushort uint16): return (T)(object)uint16;
 124                    }
 0125                    break;
 126            }
 127
 0128            ThrowHelper.ThrowJsonException();
 129            return default;
 0130        }
 131
 132        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
 0133        {
 0134            EnumConverterOptions converterOptions = _converterOptions;
 0135            if ((converterOptions & EnumConverterOptions.AllowStrings) != 0)
 0136            {
 0137                ulong key = ConvertToUInt64(value);
 138
 0139                if (_nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted))
 0140                {
 0141                    writer.WriteStringValue(formatted);
 0142                    return;
 143                }
 144
 0145                if (IsDefinedValueOrCombinationOfValues(key))
 0146                {
 0147                    Debug.Assert(s_isFlagsEnum, "Should only be entered by flags enums.");
 0148                    string stringValue = FormatEnumAsString(key, value, dictionaryKeyPolicy: null);
 0149                    if (_nameCacheForWriting.Count < NameCacheSizeSoftLimit)
 0150                    {
 0151                        formatted = JsonEncodedText.Encode(stringValue, options.Encoder);
 0152                        writer.WriteStringValue(formatted);
 0153                        _nameCacheForWriting.TryAdd(key, formatted);
 0154                    }
 155                    else
 0156                    {
 157                        // We also do not create a JsonEncodedText instance here because passing the string
 158                        // directly to the writer is cheaper than creating one and not caching it for reuse.
 0159                        writer.WriteStringValue(stringValue);
 0160                    }
 161
 0162                    return;
 163                }
 0164            }
 165
 0166            if ((converterOptions & EnumConverterOptions.AllowNumbers) == 0)
 0167            {
 0168                ThrowHelper.ThrowJsonException();
 169            }
 170
 0171            if (s_isSignedEnum)
 0172            {
 0173                writer.WriteNumberValue(ConvertToInt64(value));
 0174            }
 175            else
 0176            {
 0177                writer.WriteNumberValue(ConvertToUInt64(value));
 0178            }
 0179        }
 180
 181        internal override T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions 
 0182        {
 183            // NB JsonSerializerOptions.DictionaryKeyPolicy is ignored on deserialization.
 184            // This is true for all converters that implement dictionary key serialization.
 185
 0186            if (!TryParseEnumFromString(ref reader, out T result))
 0187            {
 0188                ThrowHelper.ThrowJsonException();
 189            }
 190
 0191            return result;
 0192        }
 193
 194        internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, T value, JsonSerializerOptions options, bo
 0195        {
 0196            JsonNamingPolicy? dictionaryKeyPolicy = options.DictionaryKeyPolicy is { } dkp && dkp != _namingPolicy ? dkp
 0197            ulong key = ConvertToUInt64(value);
 198
 0199            if (dictionaryKeyPolicy is null && _nameCacheForWriting.TryGetValue(key, out JsonEncodedText formatted))
 0200            {
 0201                writer.WritePropertyName(formatted);
 0202                return;
 203            }
 204
 0205            if (IsDefinedValueOrCombinationOfValues(key))
 0206            {
 0207                Debug.Assert(s_isFlagsEnum || dictionaryKeyPolicy is not null, "Should only be entered by flags enums or
 0208                string stringValue = FormatEnumAsString(key, value, dictionaryKeyPolicy);
 0209                if (dictionaryKeyPolicy is null && _nameCacheForWriting.Count < NameCacheSizeSoftLimit)
 0210                {
 211                    // Only attempt to cache if there is no dictionary key policy.
 0212                    formatted = JsonEncodedText.Encode(stringValue, options.Encoder);
 0213                    writer.WritePropertyName(formatted);
 0214                    _nameCacheForWriting.TryAdd(key, formatted);
 0215                }
 216                else
 0217                {
 218                    // We also do not create a JsonEncodedText instance here because passing the string
 219                    // directly to the writer is cheaper than creating one and not caching it for reuse.
 0220                    writer.WritePropertyName(stringValue);
 0221                }
 222
 0223                return;
 224            }
 225
 0226            if (s_isSignedEnum)
 0227            {
 0228                writer.WritePropertyName(ConvertToInt64(value));
 0229            }
 230            else
 0231            {
 0232                writer.WritePropertyName(key);
 0233            }
 0234        }
 235
 236        private unsafe bool TryParseEnumFromString(ref Utf8JsonReader reader, out T result)
 0237        {
 0238            Debug.Assert(reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName);
 239
 0240            int bufferLength = reader.ValueLength;
 0241            char[]? rentedBuffer = null;
 242            bool success;
 243
 0244            Span<char> charBuffer = bufferLength <= JsonConstants.StackallocCharThreshold
 0245                ? stackalloc char[JsonConstants.StackallocCharThreshold]
 0246                : (rentedBuffer = ArrayPool<char>.Shared.Rent(bufferLength));
 247
 0248            int charsWritten = reader.CopyString(charBuffer);
 0249            charBuffer = charBuffer.Slice(0, charsWritten);
 250#if NET
 0251            ReadOnlySpan<char> source = charBuffer.Trim();
 0252            ConcurrentDictionary<string, ulong>.AlternateLookup<ReadOnlySpan<char>> lookup = _nameCacheForReading.GetAlt
 253#else
 254            string source = ((ReadOnlySpan<char>)charBuffer).Trim().ToString();
 255            ConcurrentDictionary<string, ulong> lookup = _nameCacheForReading;
 256#endif
 0257            if (lookup.TryGetValue(source, out ulong key))
 0258            {
 0259                result = ConvertFromUInt64(key);
 0260                success = true;
 0261                goto End;
 262            }
 263
 0264            if (JsonHelpers.IntegerRegex.IsMatch(source))
 0265            {
 266                // We found an integer that is not an enum field name.
 0267                if ((_converterOptions & EnumConverterOptions.AllowNumbers) != 0)
 0268                {
 0269                    success = Enum.TryParse(source, out result);
 0270                }
 271                else
 0272                {
 0273                    result = default;
 0274                    success = false;
 0275                }
 0276            }
 277            else
 0278            {
 0279                success = TryParseNamedEnum(source, out result);
 0280            }
 281
 0282            if (success && _nameCacheForReading.Count < NameCacheSizeSoftLimit)
 0283            {
 0284                lookup.TryAdd(source, ConvertToUInt64(result));
 0285            }
 286
 0287        End:
 0288            if (rentedBuffer is not null)
 0289            {
 0290                charBuffer.Clear();
 0291                ArrayPool<char>.Shared.Return(rentedBuffer);
 0292            }
 293
 0294            return success;
 0295        }
 296
 297        private bool TryParseNamedEnum(
 298#if NET
 299            ReadOnlySpan<char> source,
 300#else
 301            string source,
 302#endif
 303            out T result)
 0304        {
 305#if NET
 0306            Dictionary<string, EnumFieldInfo>.AlternateLookup<ReadOnlySpan<char>> lookup = _enumFieldInfoIndex.GetAltern
 0307            ReadOnlySpan<char> rest = source;
 308#else
 309            Dictionary<string, EnumFieldInfo> lookup = _enumFieldInfoIndex;
 310            ReadOnlySpan<char> rest = source.AsSpan();
 311#endif
 0312            ulong key = 0;
 313
 314            do
 0315            {
 316                ReadOnlySpan<char> next;
 0317                int i = rest.IndexOf(',');
 0318                if (i == -1)
 0319                {
 0320                    next = rest;
 0321                    rest = default;
 0322                }
 323                else
 0324                {
 0325                    next = rest.Slice(0, i).TrimEnd();
 0326                    rest = rest.Slice(i + 1).TrimStart();
 0327                }
 328
 0329                if (lookup.TryGetValue(
 0330#if NET
 0331                        next,
 0332#else
 0333                        next.ToString(),
 0334#endif
 0335                        out EnumFieldInfo? firstResult) &&
 0336                    firstResult.GetMatchingField(next) is EnumFieldInfo match)
 0337                {
 0338                    key |= match.Key;
 0339                    continue;
 340                }
 341
 0342                result = default;
 0343                return false;
 344
 0345            } while (!rest.IsEmpty);
 346
 0347            result = ConvertFromUInt64(key);
 0348            return true;
 0349        }
 350
 351        private static ulong ConvertToUInt64(T value)
 0352        {
 0353            return s_enumTypeCode switch
 0354            {
 0355                TypeCode.Int32 => (ulong)(int)(object)value,
 0356                TypeCode.UInt32 => (uint)(object)value,
 0357                TypeCode.Int64 => (ulong)(long)(object)value,
 0358                TypeCode.UInt64 => (ulong)(object)value,
 0359                TypeCode.Int16 => (ulong)(short)(object)value,
 0360                TypeCode.UInt16 => (ushort)(object)value,
 0361                TypeCode.SByte => (ulong)(sbyte)(object)value,
 0362                _ => (byte)(object)value
 0363            };
 0364        }
 365
 366        private static long ConvertToInt64(T value)
 0367        {
 0368            Debug.Assert(s_isSignedEnum);
 0369            return s_enumTypeCode switch
 0370            {
 0371                TypeCode.Int32 => (int)(object)value,
 0372                TypeCode.Int64 => (long)(object)value,
 0373                TypeCode.Int16 => (short)(object)value,
 0374                _ => (sbyte)(object)value,
 0375            };
 0376        }
 377
 378        private static T ConvertFromUInt64(ulong value)
 0379        {
 0380            return s_enumTypeCode switch
 0381            {
 0382                TypeCode.Int32 => (T)(object)(int)value,
 0383                TypeCode.UInt32 => (T)(object)(uint)value,
 0384                TypeCode.Int64 => (T)(object)(long)value,
 0385                TypeCode.UInt64 => (T)(object)value,
 0386                TypeCode.Int16 => (T)(object)(short)value,
 0387                TypeCode.UInt16 => (T)(object)(ushort)value,
 0388                TypeCode.SByte => (T)(object)(sbyte)value,
 0389                _ => (T)(object)(byte)value
 0390            };
 0391        }
 392
 393        /// <summary>
 394        /// Attempt to format the enum value as a comma-separated string of flag values, or returns false if not a valid
 395        /// </summary>
 396        private unsafe string FormatEnumAsString(ulong key, T value, JsonNamingPolicy? dictionaryKeyPolicy)
 0397        {
 0398            Debug.Assert(IsDefinedValueOrCombinationOfValues(key), "must only be invoked against valid enum values.");
 0399            Debug.Assert(
 0400                s_isFlagsEnum || (dictionaryKeyPolicy is not null && Enum.IsDefined(typeof(T), value)),
 0401                "must either be a flag type or computing a dictionary key policy.");
 402
 0403            if (s_isFlagsEnum)
 0404            {
 0405                using ValueStringBuilder sb = new(stackalloc char[JsonConstants.StackallocCharThreshold]);
 0406                ulong remainingBits = key;
 407
 0408                foreach (EnumFieldInfo enumField in _enumFieldInfo)
 0409                {
 0410                    ulong fieldKey = enumField.Key;
 0411                    if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey)
 0412                    {
 0413                        remainingBits &= ~fieldKey;
 0414                        string name = dictionaryKeyPolicy is not null
 0415                            ? ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind)
 0416                            : enumField.JsonName;
 417
 0418                        if (sb.Length > 0)
 0419                        {
 0420                            sb.Append(", ");
 0421                        }
 422
 0423                        sb.Append(name);
 424
 0425                        if (remainingBits == 0)
 0426                        {
 0427                            break;
 428                        }
 0429                    }
 0430                }
 431
 0432                Debug.Assert(remainingBits == 0 && sb.Length > 0, "unexpected remaining bits or empty string.");
 0433                return sb.ToString();
 434            }
 435            else
 0436            {
 0437                Debug.Assert(dictionaryKeyPolicy is not null);
 438
 0439                foreach (EnumFieldInfo enumField in _enumFieldInfo)
 0440                {
 441                    // Search for an exact match and apply the key policy.
 0442                    if (enumField.Key == key)
 0443                    {
 0444                        return ResolveAndValidateJsonName(enumField.OriginalName, dictionaryKeyPolicy, enumField.Kind);
 445                    }
 0446                }
 447
 0448                Debug.Fail("should not have been reached.");
 449                return null;
 450            }
 0451        }
 452
 453        private bool IsDefinedValueOrCombinationOfValues(ulong key)
 0454        {
 0455            if (s_isFlagsEnum)
 0456            {
 0457                ulong remainingBits = key;
 458
 0459                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 0460                {
 0461                    ulong fieldKey = fieldInfo.Key;
 0462                    if (fieldKey == 0 ? key == 0 : (remainingBits & fieldKey) == fieldKey)
 0463                    {
 0464                        remainingBits &= ~fieldKey;
 465
 0466                        if (remainingBits == 0)
 0467                        {
 0468                            return true;
 469                        }
 0470                    }
 0471                }
 472
 0473                return false;
 474            }
 475            else
 0476            {
 0477                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 0478                {
 0479                    if (fieldInfo.Key == key)
 0480                    {
 0481                        return true;
 482                    }
 0483                }
 484
 0485                return false;
 486            }
 0487        }
 488
 489        internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling)
 0490        {
 0491            if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0)
 0492            {
 493                // This explicitly ignores the integer component in converters configured as AllowNumbers | AllowStrings
 494                // which is the default for JsonStringEnumConverter. This sacrifices some precision in the schema for si
 495
 0496                if (s_isFlagsEnum)
 0497                {
 498                    // Do not report enum values in case of flags.
 0499                    return new() { Type = JsonSchemaType.String };
 500                }
 501
 0502                JsonArray enumValues = [];
 0503                foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
 0504                {
 0505                    enumValues.Add((JsonNode)fieldInfo.JsonName);
 0506                }
 507
 0508                return new() { Enum = enumValues };
 509            }
 510
 0511            return new() { Type = JsonSchemaType.Integer };
 0512        }
 513
 514        internal override JsonValueType GetSupportedJsonValueTypes(JsonNumberHandling _)
 0515        {
 0516            EnumConverterOptions converterOptions = _converterOptions;
 0517            bool allowsString = (converterOptions & EnumConverterOptions.AllowStrings) != 0;
 0518            bool allowsNumber = (converterOptions & EnumConverterOptions.AllowNumbers) != 0;
 519
 0520            Debug.Assert(allowsString || allowsNumber, "EnumConverter must allow strings, numbers, or both.");
 521
 0522            return (allowsString, allowsNumber) switch
 0523            {
 0524                (true, true) => JsonValueType.String | JsonValueType.Number,
 0525                (true, false) => JsonValueType.String,
 0526                (false, true) => JsonValueType.Number,
 0527                _ => JsonValueType.Number, // Defensive: at least one must be true; default to numeric.
 0528            };
 0529        }
 530
 531        private static EnumFieldInfo[] ResolveEnumFields(JsonNamingPolicy? namingPolicy)
 0532        {
 533#if NET
 0534            string[] names = Enum.GetNames<T>();
 0535            T[] values = Enum.GetValues<T>();
 536#else
 537            string[] names = Enum.GetNames(typeof(T));
 538            T[] values = (T[])Enum.GetValues(typeof(T));
 539#endif
 0540            Debug.Assert(names.Length == values.Length);
 541
 0542            Dictionary<string, string>? enumMemberAttributes = null;
 0543            foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static))
 0544            {
 0545                if (field.GetCustomAttribute<JsonStringEnumMemberNameAttribute>() is { } attribute)
 0546                {
 0547                    (enumMemberAttributes ??= new(StringComparer.Ordinal)).Add(field.Name, attribute.Name);
 0548                }
 0549            }
 550
 0551            var enumFields = new EnumFieldInfo[names.Length];
 0552            for (int i = 0; i < names.Length; i++)
 0553            {
 0554                string originalName = names[i];
 0555                T value = values[i];
 0556                ulong key = ConvertToUInt64(value);
 557                EnumFieldNameKind kind;
 558
 0559                if (enumMemberAttributes is not null && enumMemberAttributes.TryGetValue(originalName, out string? attri
 0560                {
 0561                    originalName = attributeName;
 0562                    kind = EnumFieldNameKind.Attribute;
 0563                }
 564                else
 0565                {
 0566                    kind = namingPolicy is not null ? EnumFieldNameKind.NamingPolicy : EnumFieldNameKind.Default;
 0567                }
 568
 0569                string jsonName = ResolveAndValidateJsonName(originalName, namingPolicy, kind);
 0570                enumFields[i] = new EnumFieldInfo(key, kind, originalName, jsonName);
 0571            }
 572
 0573            if (s_isFlagsEnum)
 0574            {
 575                // Perform topological sort for flags enums to ensure values that are supersets of other values come fir
 576                // This is important for flags enums to ensure proper parsing and formatting.
 0577                enumFields = TopologicalSortEnumFields(enumFields);
 0578            }
 579
 0580            return enumFields;
 0581        }
 582
 583        private static string ResolveAndValidateJsonName(string name, JsonNamingPolicy? namingPolicy, EnumFieldNameKind 
 0584        {
 0585            if (kind is not EnumFieldNameKind.Attribute && namingPolicy is not null)
 0586            {
 587                // Do not apply a naming policy to names that are explicitly set via attributes.
 588                // This is consistent with JsonPropertyNameAttribute semantics.
 0589                name = namingPolicy.ConvertName(name);
 0590            }
 591
 0592            if (name is null ||
 0593                (name.Length > 0 && (char.IsWhiteSpace(name[0]) || char.IsWhiteSpace(name[name.Length - 1]))) ||
 0594                (s_isFlagsEnum && (name.Length == 0 || name.Contains(','))))
 0595            {
 596                // Reject null strings or strings with leading or trailing whitespace.
 597                // In the case of flags additionally reject empty strings or strings containing commas,
 598                // both of which would introduce ambiguity in flag value parsing and formatting.
 0599                ThrowHelper.ThrowInvalidOperationException_UnsupportedEnumIdentifier(typeof(T), name);
 600            }
 601
 0602            return name;
 0603        }
 604
 0605        private sealed class EnumFieldInfo(ulong key, EnumFieldNameKind kind, string originalName, string jsonName)
 606        {
 607            private List<EnumFieldInfo>? _conflictingFields;
 0608            public EnumFieldNameKind Kind { get; } = kind;
 0609            public ulong Key { get; } = key;
 0610            public string OriginalName { get; } = originalName;
 0611            public string JsonName { get; } = jsonName;
 612
 613            /// <summary>
 614            /// Assuming we have field that conflicts with the current up to case sensitivity,
 615            /// append it to a list of trailing values for use by the enum value parser.
 616            /// </summary>
 617            public void AppendConflictingField(EnumFieldInfo other)
 0618            {
 0619                Debug.Assert(JsonName.Equals(other.JsonName, StringComparison.OrdinalIgnoreCase), "The conflicting entry
 620
 0621                if (ConflictsWith(this, other))
 0622                {
 623                    // Silently discard if the preceding entry is the default or has identical name.
 0624                    return;
 625                }
 626
 0627                List<EnumFieldInfo> conflictingFields = _conflictingFields ??= [];
 628
 629                // Walk the existing list to ensure we do not add duplicates.
 0630                foreach (EnumFieldInfo conflictingField in conflictingFields)
 0631                {
 0632                    if (ConflictsWith(conflictingField, other))
 0633                    {
 0634                        return;
 635                    }
 0636                }
 637
 0638                conflictingFields.Add(other);
 639
 640                // Determines whether the first field info matches everything that the second field info matches,
 641                // in which case the second field info is redundant and doesn't need to be added to the list.
 642                static bool ConflictsWith(EnumFieldInfo current, EnumFieldInfo other)
 0643                {
 644                    // The default name matches everything case-insensitively.
 0645                    if (current.Kind is EnumFieldNameKind.Default)
 0646                    {
 0647                        return true;
 648                    }
 649
 650                    // current matches case-sensitively since it's not the default name.
 651                    // other matches case-insensitively, so it matches more than current.
 0652                    if (other.Kind is EnumFieldNameKind.Default)
 0653                    {
 0654                        return false;
 655                    }
 656
 657                    // Both are case-sensitive so they need to be identical.
 0658                    return current.JsonName.Equals(other.JsonName, StringComparison.Ordinal);
 0659                }
 0660            }
 661
 662            public EnumFieldInfo? GetMatchingField(ReadOnlySpan<char> input)
 0663            {
 0664                Debug.Assert(input.Equals(JsonName.AsSpan(), StringComparison.OrdinalIgnoreCase), "Must equal the field 
 665
 0666                if (Kind is EnumFieldNameKind.Default || input.SequenceEqual(JsonName.AsSpan()))
 0667                {
 668                    // Default enum names use case insensitive parsing so are always a match.
 0669                    return this;
 670                }
 671
 0672                if (_conflictingFields is { } conflictingFields)
 0673                {
 0674                    Debug.Assert(conflictingFields.Count > 0);
 0675                    foreach (EnumFieldInfo matchingField in conflictingFields)
 0676                    {
 0677                        if (matchingField.Kind is EnumFieldNameKind.Default || input.SequenceEqual(matchingField.JsonNam
 0678                        {
 0679                            return matchingField;
 680                        }
 0681                    }
 0682                }
 683
 0684                return null;
 0685            }
 686        }
 687
 688        /// <summary>
 689        /// Performs a topological sort on enum fields to ensure values that are supersets of other values come first.
 690        /// </summary>
 691        private static EnumFieldInfo[] TopologicalSortEnumFields(EnumFieldInfo[] enumFields)
 0692        {
 0693            if (enumFields.Length <= 1)
 0694            {
 0695                return enumFields;
 696            }
 697
 0698            var indices = new (int negativePopCount, int index)[enumFields.Length];
 0699            for (int i = 0; i < enumFields.Length; i++)
 0700            {
 701                // We want values with more bits set to come first so negate the pop count.
 702                // Keep the index as a second comparand so that sorting stability is preserved.
 0703                indices[i] = (-PopCount(enumFields[i].Key), i);
 0704            }
 705
 0706            Array.Sort(indices);
 707
 0708            var sortedFields = new EnumFieldInfo[enumFields.Length];
 0709            for (int i = 0; i < indices.Length; i++)
 0710            {
 711                // extract the index from the sorted tuple
 0712                int index = indices[i].index;
 0713                sortedFields[i] = enumFields[index];
 0714            }
 715
 0716            return sortedFields;
 0717        }
 718
 719        private static int PopCount(ulong value)
 0720        {
 721#if NET
 0722            return (int)ulong.PopCount(value);
 723#else
 724            int count = 0;
 725            while (value != 0)
 726            {
 727                value &= value - 1;
 728                count++;
 729            }
 730            return count;
 731#endif
 0732        }
 733
 734        private enum EnumFieldNameKind
 735        {
 736            Default = 0,
 737            NamingPolicy = 1,
 738            Attribute = 2,
 739        }
 740    }
 741}
 742

Methods/Properties

.cctor()
.ctor(System.Text.Json.Serialization.Converters.EnumConverterOptions,System.Text.Json.JsonNamingPolicy,System.Text.Json.JsonSerializerOptions)
AddToEnumFieldIndex(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>)
Read(System.Text.Json.Utf8JsonReader&,System.Type,System.Text.Json.JsonSerializerOptions)
Write(System.Text.Json.Utf8JsonWriter,T,System.Text.Json.JsonSerializerOptions)
ReadAsPropertyNameCore(System.Text.Json.Utf8JsonReader&,System.Type,System.Text.Json.JsonSerializerOptions)
WriteAsPropertyNameCore(System.Text.Json.Utf8JsonWriter,T,System.Text.Json.JsonSerializerOptions,System.Boolean)
TryParseEnumFromString(System.Text.Json.Utf8JsonReader&,T&)
TryParseNamedEnum(System.ReadOnlySpan`1<System.Char>,T&)
ConvertToUInt64(T)
ConvertToInt64(T)
ConvertFromUInt64(System.UInt64)
FormatEnumAsString(System.UInt64,T,System.Text.Json.JsonNamingPolicy)
IsDefinedValueOrCombinationOfValues(System.UInt64)
GetSchema(System.Text.Json.Serialization.JsonNumberHandling)
GetSupportedJsonValueTypes(System.Text.Json.Serialization.JsonNumberHandling)
ResolveEnumFields(System.Text.Json.JsonNamingPolicy)
ResolveAndValidateJsonName(System.String,System.Text.Json.JsonNamingPolicy,System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldNameKind<T>)
.ctor(System.UInt64,System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldNameKind<T>,System.String,System.String)
Kind()
Key()
OriginalName()
JsonName()
AppendConflictingField(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>)
ConflictsWith(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>,System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>)
GetMatchingField(System.ReadOnlySpan`1<System.Char>)
TopologicalSortEnumFields(System.Text.Json.Serialization.Converters.EnumConverter`1/EnumFieldInfo<T>[])
PopCount(System.UInt64)