< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 463
Coverable lines: 463
Total lines: 749
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 171
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/JsonUnionTypeStructuralClassifier.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.Generic;
 6using System.Diagnostics;
 7#if NET
 8using System.Runtime.CompilerServices;
 9#endif
 10using System.Text;
 11using System.Text.Json.Serialization.Metadata;
 12
 13namespace System.Text.Json.Serialization
 14{
 15    /// <summary>
 16    /// Classifies JSON payloads into union case types by comparing their JSON value types
 17    /// and, for JSON objects, their property names.
 18    /// </summary>
 19    /// <remarks>
 20    /// <para>
 21    /// The default union classification distinguishes cases that use different JSON token kinds,
 22    /// such as a string and an array. This classifier adds structural classification for .NET
 23    /// POCOs configured to serialize as JSON objects.
 24    /// </para>
 25    /// <para>
 26    /// To classify an object, the classifier starts with every POCO case as a candidate. For each
 27    /// property name at the level of the current JSON object, it eliminates candidates that do not
 28    /// define a matching property. Property values and nested content are not examined, and
 29    /// property order does not affect the result. Name matching honors
 30    /// <see cref="JsonSerializerOptions.PropertyNameCaseInsensitive"/>.
 31    /// </para>
 32    /// <para>
 33    /// After reading the object, candidates missing a required property are eliminated. Missing
 34    /// optional properties have no effect. A property name that is not defined by any case
 35    /// eliminates only cases configured with <see cref="JsonUnmappedMemberHandling.Disallow"/>.
 36    /// </para>
 37    /// <para>
 38    /// The classifier selects the case when exactly one candidate remains. Classification fails
 39    /// when no candidates or multiple candidates remain. Configurations containing a case that can
 40    /// never be selected uniquely are rejected when the classifier is created.
 41    /// </para>
 42    /// <para>
 43    /// This classifier supports union types only. Nested union cases, polymorphic cases, and
 44    /// reference-preserving deserialization are not supported.
 45    /// </para>
 46    /// </remarks>
 47    public class JsonUnionTypeStructuralClassifier : JsonTypeClassifierFactory
 48    {
 49        /// <summary>
 50        /// Initializes a new instance of the <see cref="JsonUnionTypeStructuralClassifier"/> class.
 51        /// </summary>
 052        public JsonUnionTypeStructuralClassifier()
 053        {
 054        }
 55
 56        /// <inheritdoc/>
 57        public override bool CanClassify(JsonTypeClassifierContext context)
 058        {
 059            ArgumentNullException.ThrowIfNull(context);
 060            return context.Kind is JsonTypeClassifierKind.Union;
 061        }
 62
 63        /// <inheritdoc/>
 64        public override JsonTypeClassifier CreateJsonClassifier(
 65            JsonTypeClassifierContext context,
 66            JsonSerializerOptions options)
 067        {
 068            ArgumentNullException.ThrowIfNull(context);
 069            ArgumentNullException.ThrowIfNull(options);
 70
 071            if (context.Kind is not JsonTypeClassifierKind.Union)
 072            {
 073                ThrowHelper.ThrowInvalidOperationException_UnionTypeStructuralClassifierOnlyForUnions(context.DeclaringT
 74            }
 75
 076            if (options.ReferenceHandlingStrategy is JsonKnownReferenceHandler.Preserve)
 077            {
 078                ThrowHelper.ThrowNotSupportedException_UnionTypeStructuralClassifierPreserveReferencesNotSupported(conte
 79            }
 80
 081            StructuralClassifier classifier = BuildStructuralClassifier(context.DeclaringTypeInfo, context.UnionCases, o
 082            return classifier.Classify;
 083        }
 84
 85        private static StructuralClassifier BuildStructuralClassifier(
 86            JsonTypeInfo unionTypeInfo,
 87            IReadOnlyList<JsonUnionCaseInfo> unionCases,
 88            JsonSerializerOptions options)
 089        {
 090            Type unionType = unionTypeInfo.Type;
 091            JsonNumberHandling? unionNumberHandling = unionTypeInfo.NumberHandling;
 92
 93            // POCO object cases expose JsonPropertyInfo metadata through JsonTypeInfoKind.Object.
 94            // A non-POCO JSON object case advertises the Object shape without such metadata.
 095            Dictionary<JsonValueType, Type> shapeBasedCases = new();
 096            List<PocoObjectCase> pocoObjectCaseList = [];
 097            int requiredPropertyCount = 0;
 98
 099            foreach (JsonUnionCaseInfo unionCase in unionCases)
 0100            {
 0101                AddCase(
 0102                    unionType,
 0103                    unionCase.CaseType,
 0104                    unionNumberHandling,
 0105                    options,
 0106                    shapeBasedCases,
 0107                    pocoObjectCaseList,
 0108                    ref requiredPropertyCount);
 0109            }
 110
 0111            shapeBasedCases.TryGetValue(JsonValueType.Object, out Type? nonPocoJsonObjectCaseType);
 0112            if (nonPocoJsonObjectCaseType is not null &&
 0113                pocoObjectCaseList is { Count: > 0 })
 0114            {
 0115                ThrowHelper.ThrowNotSupportedException_UnionTypeStructuralClassifierAmbiguousCases(
 0116                    unionType,
 0117                    pocoObjectCaseList[0].CaseType,
 0118                    nonPocoJsonObjectCaseType,
 0119                    JsonValueType.Object);
 120            }
 121
 0122            ValidatePocoObjectCases(unionType, pocoObjectCaseList);
 123
 0124            PocoObjectCase[] pocoObjectCases = [..pocoObjectCaseList];
 0125            Dictionary<byte[], List<PocoPropertyClassifierInfo>> pocoPropertyIndex = new(JsonHelpers.ByteArrayOrdinalCom
 0126            Dictionary<string, List<PocoPropertyClassifierInfo>>? caseInsensitivePocoPropertyIndex = null;
 127
 128            // Keep the ordinal and case-insensitive indexes separate so exact matches can be
 129            // processed first when differently-cased names overlap.
 0130            foreach (PocoObjectCase pocoObjectCase in pocoObjectCases)
 0131            {
 0132                foreach (PocoPropertyClassifierInfo property in pocoObjectCase.Properties)
 0133                {
 0134                    AddPocoPropertyInfo(pocoPropertyIndex, property.NameAsUtf8Bytes, property);
 135
 0136                    if (!property.IsCaseSensitive)
 0137                    {
 0138                        caseInsensitivePocoPropertyIndex ??= new(StringComparer.OrdinalIgnoreCase);
 0139                        AddPocoPropertyInfo(caseInsensitivePocoPropertyIndex, property.Name, property);
 0140                    }
 0141                }
 0142            }
 143
 0144            return new(
 0145                shapeBasedCases,
 0146                pocoObjectCases,
 0147                pocoPropertyIndex,
 0148                caseInsensitivePocoPropertyIndex,
 0149                requiredPropertyCount);
 150
 151            static void AddPocoPropertyInfo<TKey>(
 152                Dictionary<TKey, List<PocoPropertyClassifierInfo>> index,
 153                TKey key,
 154                PocoPropertyClassifierInfo propertyInfo)
 155                where TKey : notnull
 0156            {
 0157                if (!index.TryGetValue(key, out List<PocoPropertyClassifierInfo>? matches))
 0158                {
 0159                    matches = [];
 0160                    index.Add(key, matches);
 0161                }
 162
 0163                matches.Add(propertyInfo);
 0164            }
 0165        }
 166
 167        private static void AddCase(
 168            Type unionType,
 169            Type caseType,
 170            JsonNumberHandling? unionNumberHandling,
 171            JsonSerializerOptions options,
 172            Dictionary<JsonValueType, Type> shapeBasedCases,
 173            List<PocoObjectCase> pocoObjectCases,
 174            ref int requiredPropertyCount)
 0175        {
 0176            JsonTypeInfo typeInfo = options.GetTypeInfo(caseType);
 0177            JsonNumberHandling? numberHandlingOverride = unionNumberHandling ?? typeInfo.NumberHandling;
 0178            if (typeInfo is { IsNullable: true, ElementTypeInfo: JsonTypeInfo elementTypeInfo })
 0179            {
 0180                typeInfo = elementTypeInfo;
 0181            }
 182
 0183            if (typeInfo.Kind is JsonTypeInfoKind.Union)
 0184            {
 0185                ThrowHelper.ThrowNotSupportedException_UnionTypeStructuralClassifierCaseNotSupported(
 0186                    unionType,
 0187                    caseType);
 188            }
 189
 0190            if (typeInfo.PolymorphismOptions is not null)
 0191            {
 0192                ThrowHelper.ThrowNotSupportedException_UnionTypeStructuralClassifierCaseNotSupported(
 0193                    unionType,
 0194                    caseType);
 195            }
 196
 0197            JsonNumberHandling numberHandling = numberHandlingOverride ?? typeInfo.NumberHandling ?? options.NumberHandl
 0198            JsonValueType valueTypes = typeInfo.Converter.GetSupportedJsonValueTypes(numberHandling);
 199
 0200            bool isPocoObjectCase = typeInfo is
 0201            {
 0202                Kind: JsonTypeInfoKind.Object,
 0203                Converter.IsInternalConverter: true,
 0204            };
 205
 0206            ReadOnlySpan<JsonValueType> supportedValueTypes =
 0207            [
 0208                JsonValueType.Object,
 0209                JsonValueType.Array,
 0210                JsonValueType.String,
 0211                JsonValueType.Number,
 0212                JsonValueType.Boolean,
 0213            ];
 214
 215            // Token kind is the only discriminator for non-object shapes, so only one case can
 216            // claim each kind. POCO object cases instead participate in property matching.
 0217            Debug.Assert((valueTypes &
 0218                (JsonValueType.Object |
 0219                 JsonValueType.Array |
 0220                 JsonValueType.String |
 0221                 JsonValueType.Number |
 0222                 JsonValueType.Boolean)) is not 0);
 223
 0224            foreach (JsonValueType valueType in supportedValueTypes)
 0225            {
 0226                if ((valueTypes & valueType) is 0)
 0227                {
 0228                    continue;
 229                }
 230
 0231                if (valueType is JsonValueType.Object && isPocoObjectCase)
 0232                {
 0233                    pocoObjectCases.Add(BuildPocoObjectCase(
 0234                        caseType,
 0235                        typeInfo,
 0236                        options,
 0237                        pocoObjectCases.Count,
 0238                        ref requiredPropertyCount));
 0239                }
 0240                else if (!shapeBasedCases.TryAdd(valueType, caseType))
 0241                {
 0242                    Type conflictingCaseType = shapeBasedCases[valueType];
 0243                    ThrowHelper.ThrowNotSupportedException_UnionTypeStructuralClassifierAmbiguousCases(
 0244                        unionType,
 0245                        conflictingCaseType,
 0246                        caseType,
 0247                        valueType);
 248                }
 0249            }
 0250        }
 251
 252        private static PocoObjectCase BuildPocoObjectCase(
 253            Type caseType,
 254            JsonTypeInfo typeInfo,
 255            JsonSerializerOptions options,
 256            int pocoCaseIndex,
 257            ref int requiredPropertyCount)
 0258        {
 0259            Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object);
 260
 0261            bool caseInsensitive = options.PropertyNameCaseInsensitive;
 0262            List<PocoPropertyClassifierInfo> properties = new(typeInfo.Properties.Count);
 0263            int requiredCount = 0;
 0264            bool hasExtensionData = false;
 265
 0266            foreach (JsonPropertyInfo property in typeInfo.Properties)
 0267            {
 0268                if (property.IsExtensionData)
 0269                {
 0270                    hasExtensionData = true;
 0271                    continue;
 272                }
 273
 0274                int requiredPropertyIndex = property.IsRequired ? requiredPropertyCount++ : -1;
 0275                properties.Add(new(
 0276                    property.Name,
 0277                    property.IsRequired,
 0278                    isCaseSensitive: !caseInsensitive,
 0279                    pocoCaseIndex,
 0280                    requiredPropertyIndex));
 281
 0282                if (property.IsRequired)
 0283                {
 0284                    requiredCount++;
 0285                }
 0286            }
 287
 0288            JsonUnmappedMemberHandling unmappedMemberHandling = typeInfo.UnmappedMemberHandling ??
 0289                (hasExtensionData ? JsonUnmappedMemberHandling.Skip : options.UnmappedMemberHandling);
 290
 0291            return new(
 0292                caseType,
 0293                [..properties],
 0294                unmappedMemberHandling is JsonUnmappedMemberHandling.Disallow,
 0295                requiredCount);
 0296        }
 297
 298        private static void ValidatePocoObjectCases(
 299            Type unionType,
 300            List<PocoObjectCase> pocoObjectCases)
 0301        {
 302            // Reject a case when every object it can accept also satisfies another case, since it
 303            // can never be selected uniquely. This quadratic check runs only during construction
 304            // and is bounded by the union cases declared on the type.
 0305            for (int i = 0; i < pocoObjectCases.Count; i++)
 0306            {
 0307                PocoObjectCase pocoObjectCase = pocoObjectCases[i];
 308
 0309                for (int j = 0; j < pocoObjectCases.Count; j++)
 0310                {
 0311                    if (i == j)
 0312                    {
 0313                        continue;
 314                    }
 315
 0316                    PocoObjectCase other = pocoObjectCases[j];
 0317                    if (pocoObjectCase.IsShadowedBy(other))
 0318                    {
 0319                        ThrowHelper.ThrowNotSupportedException_UnionTypeStructuralClassifierUnreachableObjectCase(
 0320                            unionType,
 0321                            pocoObjectCase.CaseType,
 0322                            other.CaseType);
 323                    }
 0324                }
 0325            }
 326
 0327        }
 328
 329        /// <summary>Classifies JSON values using precomputed union case metadata.</summary>
 330        private sealed class StructuralClassifier
 331        {
 332            // PocoObjectCaseState is 8 bytes, so this caps the stack-allocated buffer at 128 bytes.
 333            private const int InlinePocoObjectCaseCount = 16;
 334
 335            private readonly Dictionary<byte[], List<PocoPropertyClassifierInfo>> _pocoPropertyIndex;
 336            private readonly Dictionary<string, List<PocoPropertyClassifierInfo>>? _caseInsensitivePocoPropertyIndex;
 337            private readonly int[] _disallowUnmappedPropertiesPocoCaseIndices;
 338            private readonly Dictionary<JsonValueType, Type> _shapeBasedCases;
 339            private readonly PocoObjectCase[] _pocoObjectCases;
 340            private readonly int _requiredPropertyCount;
 341
 0342            public StructuralClassifier(
 0343                Dictionary<JsonValueType, Type> shapeBasedCases,
 0344                PocoObjectCase[] pocoObjectCases,
 0345                Dictionary<byte[], List<PocoPropertyClassifierInfo>> pocoPropertyIndex,
 0346                Dictionary<string, List<PocoPropertyClassifierInfo>>? caseInsensitivePocoPropertyIndex,
 0347                int requiredPropertyCount)
 0348            {
 0349                _shapeBasedCases = shapeBasedCases;
 0350                _pocoObjectCases = pocoObjectCases;
 0351                _pocoPropertyIndex = pocoPropertyIndex;
 0352                _caseInsensitivePocoPropertyIndex = caseInsensitivePocoPropertyIndex;
 0353                _requiredPropertyCount = requiredPropertyCount;
 354
 0355                List<int>? disallowUnmappedPropertiesPocoCaseIndices = null;
 0356                for (int i = 0; i < pocoObjectCases.Length; i++)
 0357                {
 0358                    if (pocoObjectCases[i].DisallowUnmappedProperties)
 0359                    {
 0360                        (disallowUnmappedPropertiesPocoCaseIndices ??= []).Add(i);
 0361                    }
 0362                }
 363
 0364                _disallowUnmappedPropertiesPocoCaseIndices = disallowUnmappedPropertiesPocoCaseIndices?.ToArray() ?? [];
 0365            }
 366
 367            public Type? Classify(ref Utf8JsonReader reader)
 0368            {
 0369                JsonValueType valueType = reader.TokenType switch
 0370                {
 0371                    JsonTokenType.StartObject => JsonValueType.Object,
 0372                    JsonTokenType.StartArray => JsonValueType.Array,
 0373                    JsonTokenType.String => JsonValueType.String,
 0374                    JsonTokenType.Number => JsonValueType.Number,
 0375                    JsonTokenType.True or JsonTokenType.False => JsonValueType.Boolean,
 0376                    _ => JsonValueType.None,
 0377                };
 378
 0379                if (valueType is JsonValueType.Object && _pocoObjectCases is { Length: > 0 })
 0380                {
 0381                    return ClassifyJsonObject(ref reader);
 382                }
 383
 0384                _shapeBasedCases.TryGetValue(valueType, out Type? caseType);
 0385                return caseType;
 0386            }
 387
 388            private Type? ClassifyJsonObject(ref Utf8JsonReader reader)
 0389            {
 0390                Debug.Assert(reader.TokenType is JsonTokenType.StartObject);
 0391                Debug.Assert(_pocoObjectCases.Length is > 0);
 392
 393                // Begin with every POCO object case as a candidate and eliminate cases using property
 394                // names only. After the complete object is scanned, required properties and
 395                // uniqueness determine the result.
 0396                PocoObjectCaseState[]? rentedPocoCaseStates = null;
 397                try
 0398                {
 0399                    scoped Span<PocoObjectCaseState> pocoCaseStates =
 0400                        _pocoObjectCases.Length <= InlinePocoObjectCaseCount
 0401                            ? stackalloc PocoObjectCaseState[InlinePocoObjectCaseCount]
 0402                            : (rentedPocoCaseStates = ArrayPool<PocoObjectCaseState>.Shared.Rent(_pocoObjectCases.Length
 0403                    pocoCaseStates = pocoCaseStates.Slice(0, _pocoObjectCases.Length);
 0404                    pocoCaseStates.Clear();
 405
 0406                    scoped Span<ulong> pocoCaseCandidateBuffer = stackalloc ulong[ValueBitArray.ScratchBufferSize];
 0407                    ValueBitArray isPocoCaseCandidate = new(
 0408                        _pocoObjectCases.Length,
 0409                        pocoCaseCandidateBuffer,
 0410                        initialWordValue: ulong.MaxValue);
 411
 0412                    scoped Span<ulong> pocoCaseMatchesCurrentPropertyBuffer =
 0413                        stackalloc ulong[ValueBitArray.ScratchBufferSize];
 0414                    ValueBitArray pocoCaseMatchesCurrentProperty =
 0415                        new(_pocoObjectCases.Length, pocoCaseMatchesCurrentPropertyBuffer);
 416
 0417                    scoped Span<ulong> requiredPropertySeenBuffer = stackalloc ulong[ValueBitArray.ScratchBufferSize];
 0418                    ValueBitArray isRequiredPropertySeen =
 0419                        new(_requiredPropertyCount, requiredPropertySeenBuffer);
 0420                    int i = 0;
 421
 0422                    while (true)
 0423                    {
 0424                        reader.ReadWithVerify();
 0425                        Debug.Assert(reader.TokenType is JsonTokenType.PropertyName or JsonTokenType.EndObject);
 0426                        if (reader.TokenType is JsonTokenType.EndObject)
 0427                        {
 0428                            return SelectJsonObjectCase(
 0429                                _pocoObjectCases,
 0430                                pocoCaseStates,
 0431                                isPocoCaseCandidate);
 432                        }
 433
 0434                        ReadNextProperty(
 0435                            ref reader,
 0436                            propertyIndex: i++,
 0437                            pocoCaseStates,
 0438                            isPocoCaseCandidate,
 0439                            pocoCaseMatchesCurrentProperty,
 0440                            isRequiredPropertySeen);
 0441                    }
 442                }
 443                finally
 0444                {
 0445                    if (rentedPocoCaseStates is not null)
 0446                    {
 447#if NET
 0448                        Debug.Assert(!RuntimeHelpers.IsReferenceOrContainsReferences<PocoObjectCaseState>());
 449#endif
 0450                        ArrayPool<PocoObjectCaseState>.Shared.Return(
 0451                            rentedPocoCaseStates,
 0452                            clearArray: false);
 0453                    }
 0454                }
 455
 456                static Type? SelectJsonObjectCase(
 457                    PocoObjectCase[] pocoObjectCases,
 458                    scoped ReadOnlySpan<PocoObjectCaseState> pocoCaseStates,
 459                    scoped ValueBitArray isPocoCaseCandidate)
 0460                {
 0461                    Type? selectedType = null;
 462
 0463                    for (int i = 0; i < pocoObjectCases.Length; i++)
 0464                    {
 0465                        PocoObjectCase pocoObjectCase = pocoObjectCases[i];
 0466                        PocoObjectCaseState pocoCaseState = pocoCaseStates[i];
 0467                        if (!isPocoCaseCandidate[i] ||
 0468                            pocoCaseState.RequiredSeen < pocoObjectCase.RequiredCount)
 0469                        {
 0470                            continue;
 471                        }
 472
 0473                        if (selectedType is not null)
 0474                        {
 0475                            return null;
 476                        }
 477
 0478                        selectedType = pocoObjectCase.CaseType;
 0479                    }
 480
 0481                    return selectedType;
 0482                }
 0483            }
 484
 485            private void ReadNextProperty(
 486                scoped ref Utf8JsonReader reader,
 487                int propertyIndex,
 488                scoped Span<PocoObjectCaseState> pocoCaseStates,
 489                scoped ValueBitArray isPocoCaseCandidate,
 490                scoped ValueBitArray pocoCaseMatchesCurrentProperty,
 491                scoped ValueBitArray isRequiredPropertySeen)
 0492            {
 0493                byte[]? rentedPropertyName = null;
 494                try
 0495                {
 0496                    scoped Span<byte> buffer = default;
 497                    scoped ReadOnlySpan<byte> propertyName;
 498
 0499                    if (reader is { HasValueSequence: false, ValueIsEscaped: false })
 0500                    {
 0501                        propertyName = reader.ValueSpan;
 0502                    }
 503                    else
 0504                    {
 0505                        int bufferLength = reader.ValueLength;
 0506                        buffer = bufferLength is <= JsonConstants.StackallocByteThreshold
 0507                            ? stackalloc byte[JsonConstants.StackallocByteThreshold]
 0508                            : (rentedPropertyName = ArrayPool<byte>.Shared.Rent(bufferLength));
 509
 0510                        int bytesWritten = reader.CopyString(buffer);
 0511                        propertyName = buffer.Slice(0, bytesWritten);
 0512                    }
 513
 0514                    reader.ReadWithVerify();
 515
 0516                    pocoCaseMatchesCurrentProperty.Clear();
 0517                    RecordPropertyMatches(
 0518                        propertyName,
 0519                        propertyIndex,
 0520                        pocoCaseStates,
 0521                        isPocoCaseCandidate,
 0522                        pocoCaseMatchesCurrentProperty,
 0523                        isRequiredPropertySeen);
 524
 0525                    reader.SkipWithVerify();
 0526                }
 527                finally
 0528                {
 0529                    if (rentedPropertyName is not null)
 0530                    {
 0531                        ArrayPool<byte>.Shared.Return(rentedPropertyName);
 0532                    }
 0533                }
 0534            }
 535
 536            private void RecordPropertyMatches(
 537                scoped ReadOnlySpan<byte> propertyName,
 538                int propertyIndex,
 539                scoped Span<PocoObjectCaseState> pocoCaseStates,
 540                scoped ValueBitArray isPocoCaseCandidate,
 541                scoped ValueBitArray pocoCaseMatchesCurrentProperty,
 542                scoped ValueBitArray isRequiredPropertySeen)
 0543            {
 544                // A name known to any POCO case retains only the POCO cases that declare it; an
 545                // unknown name retains only POCO cases that permit unmapped properties.
 0546                Debug.Assert(pocoCaseMatchesCurrentProperty.IsEmpty);
 0547                bool isKnownPocoProperty = false;
 548
 0549                if (_pocoPropertyIndex.TryLookupUtf8Key(
 0550                    propertyName,
 0551                    out List<PocoPropertyClassifierInfo>? propertyMatches))
 0552                {
 0553                    isKnownPocoProperty = true;
 554
 0555                    foreach (PocoPropertyClassifierInfo propertyInfo in propertyMatches)
 0556                    {
 0557                        RecordPropertyMatch(
 0558                            propertyInfo,
 0559                            propertyIndex,
 0560                            pocoCaseStates,
 0561                            isPocoCaseCandidate,
 0562                            pocoCaseMatchesCurrentProperty,
 0563                            isRequiredPropertySeen);
 0564                    }
 0565                }
 566
 0567                if (_caseInsensitivePocoPropertyIndex is { } caseInsensitivePocoPropertyIndex &&
 0568                    caseInsensitivePocoPropertyIndex.TryLookupUtf8Key(
 0569                        propertyName,
 0570                        out List<PocoPropertyClassifierInfo>? caseInsensitivePropertyMatches))
 0571                {
 0572                    isKnownPocoProperty = true;
 573
 0574                    foreach (PocoPropertyClassifierInfo propertyInfo in caseInsensitivePropertyMatches)
 0575                    {
 0576                        RecordPropertyMatch(
 0577                            propertyInfo,
 0578                            propertyIndex,
 0579                            pocoCaseStates,
 0580                            isPocoCaseCandidate,
 0581                            pocoCaseMatchesCurrentProperty,
 0582                            isRequiredPropertySeen);
 0583                    }
 0584                }
 585
 0586                if (isKnownPocoProperty)
 0587                {
 588                    // An exact or case-insensitive lookup recognized the name, so retain only
 589                    // cases whose configured property metadata matched it.
 0590                    isPocoCaseCandidate.IntersectWith(pocoCaseMatchesCurrentProperty);
 0591                }
 592                else
 0593                {
 594                    // No POCO case recognizes the name, so it eliminates only cases that reject
 595                    // unmapped properties.
 0596                    foreach (int pocoCaseIndex in _disallowUnmappedPropertiesPocoCaseIndices)
 0597                    {
 0598                        isPocoCaseCandidate[pocoCaseIndex] = false;
 0599                    }
 0600                }
 601
 602                static void RecordPropertyMatch(
 603                    PocoPropertyClassifierInfo propertyInfo,
 604                    int propertyIndex,
 605                    scoped Span<PocoObjectCaseState> pocoCaseStates,
 606                    scoped ValueBitArray isPocoCaseCandidate,
 607                    scoped ValueBitArray pocoCaseMatchesCurrentProperty,
 608                    scoped ValueBitArray isRequiredPropertySeen)
 0609                {
 0610                    ref PocoObjectCaseState pocoCaseState = ref pocoCaseStates[propertyInfo.PocoCaseIndex];
 0611                    pocoCaseMatchesCurrentProperty[propertyInfo.PocoCaseIndex] = true;
 612
 613                    // Exact matches take precedence over case-insensitive matches, and one JSON
 614                    // property can contribute to at most one declared property per POCO object case.
 0615                    if (!isPocoCaseCandidate[propertyInfo.PocoCaseIndex] ||
 0616                        pocoCaseState.LastMatchedPropertyIndex == propertyIndex)
 0617                    {
 0618                        return;
 619                    }
 620
 0621                    pocoCaseState.LastMatchedPropertyIndex = propertyIndex;
 622                    // RequiredPropertyIndex is unique per declared property, so duplicate JSON
 623                    // names cannot satisfy the same requirement more than once.
 0624                    int requiredPropertyIndex = propertyInfo.RequiredPropertyIndex;
 0625                    if (propertyInfo.IsRequired &&
 0626                        !isRequiredPropertySeen[requiredPropertyIndex])
 0627                    {
 0628                        isRequiredPropertySeen[requiredPropertyIndex] = true;
 0629                        pocoCaseState.RequiredSeen++;
 0630                    }
 0631                }
 0632            }
 633        }
 634
 635        /// <summary>Describes the POCO property contract used to classify a union case.</summary>
 0636        private sealed class PocoObjectCase(
 0637            Type caseType,
 0638            PocoPropertyClassifierInfo[] properties,
 0639            bool disallowUnmappedProperties,
 0640            int requiredCount)
 641        {
 0642            public Type CaseType { get; } = caseType;
 0643            public bool DisallowUnmappedProperties { get; } = disallowUnmappedProperties;
 0644            public PocoPropertyClassifierInfo[] Properties { get; } = properties;
 0645            public int RequiredCount { get; } = requiredCount;
 646
 647            public bool IsShadowedBy(PocoObjectCase other)
 0648            {
 649                // This case is shadowed when every payload it accepts is also accepted by the
 650                // other case: the other case must be at least as permissive about unknown names,
 651                // recognize every property-name spelling accepted by this case, and require no
 652                // property that this case does not itself require.
 0653                if (!DisallowUnmappedProperties && other.DisallowUnmappedProperties)
 0654                {
 0655                    return false;
 656                }
 657
 0658                foreach (PocoPropertyClassifierInfo property in Properties)
 0659                {
 0660                    bool otherHasEquivalentProperty = false;
 0661                    foreach (PocoPropertyClassifierInfo otherProperty in other.Properties)
 0662                    {
 0663                        if (property.IsPropertyNameEquivalent(otherProperty))
 0664                        {
 0665                            otherHasEquivalentProperty = true;
 0666                            break;
 667                        }
 0668                    }
 669
 0670                    if (!otherHasEquivalentProperty)
 0671                    {
 0672                        return false;
 673                    }
 0674                }
 675
 0676                foreach (PocoPropertyClassifierInfo otherProperty in other.Properties)
 0677                {
 0678                    if (!otherProperty.IsRequired)
 0679                    {
 0680                        continue;
 681                    }
 682
 0683                    bool requiredPropertyIsGuaranteedByThisCase = false;
 0684                    foreach (PocoPropertyClassifierInfo property in Properties)
 0685                    {
 0686                        if (property.IsRequired && property.IsPropertyNameEquivalent(otherProperty))
 0687                        {
 0688                            requiredPropertyIsGuaranteedByThisCase = true;
 0689                            break;
 690                        }
 0691                    }
 692
 0693                    if (!requiredPropertyIsGuaranteedByThisCase)
 0694                    {
 0695                        return false;
 696                    }
 0697                }
 698
 0699                return true;
 0700            }
 701        }
 702
 703        /// <summary>Describes how a JSON property name contributes to a POCO object case.</summary>
 0704        private sealed class PocoPropertyClassifierInfo(
 0705            string name,
 0706            bool isRequired,
 0707            bool isCaseSensitive,
 0708            int pocoCaseIndex,
 0709            int requiredPropertyIndex)
 710        {
 0711            public int PocoCaseIndex { get; } = pocoCaseIndex;
 0712            public bool IsCaseSensitive { get; } = isCaseSensitive;
 0713            public bool IsRequired { get; } = isRequired;
 0714            public string Name { get; } = name;
 0715            public byte[] NameAsUtf8Bytes { get; } = Encoding.UTF8.GetBytes(name);
 0716            public int RequiredPropertyIndex { get; } = requiredPropertyIndex;
 717
 718            public bool IsPropertyNameEquivalent(PocoPropertyClassifierInfo other)
 0719            {
 0720                Debug.Assert(IsCaseSensitive == other.IsCaseSensitive);
 721
 0722                return string.Equals(
 0723                    Name,
 0724                    other.Name,
 0725                    IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
 0726            }
 727        }
 728
 729        /// <summary>Tracks per-case matching state while scanning a POCO JSON object.</summary>
 730        private struct PocoObjectCaseState
 731        {
 732            public int RequiredSeen;
 733
 734            /// <summary>
 735            /// Gets or sets the zero-based index of the last matched JSON property, or -1 if none matched.
 736            /// </summary>
 737            public int LastMatchedPropertyIndex
 738            {
 0739                readonly get => field - 1;
 740                set
 0741                {
 0742                    Debug.Assert(value >= 0);
 0743                    field = value + 1;
 0744                }
 745            }
 746        }
 747    }
 748}
 749

Methods/Properties

.ctor()
CanClassify(System.Text.Json.Serialization.JsonTypeClassifierContext)
CreateJsonClassifier(System.Text.Json.Serialization.JsonTypeClassifierContext,System.Text.Json.JsonSerializerOptions)
BuildStructuralClassifier(System.Text.Json.Serialization.Metadata.JsonTypeInfo,System.Collections.Generic.IReadOnlyList`1<System.Text.Json.Serialization.Metadata.JsonUnionCaseInfo>,System.Text.Json.JsonSerializerOptions)
AddPocoPropertyInfo(System.Collections.Generic.Dictionary`2<TKey,System.Collections.Generic.List`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoPropertyClassifierInfo>>,TKey,System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoPropertyClassifierInfo)
AddCase(System.Type,System.Type,System.Nullable`1<System.Text.Json.Serialization.JsonNumberHandling>,System.Text.Json.JsonSerializerOptions,System.Collections.Generic.Dictionary`2<System.Text.Json.Serialization.JsonValueType,System.Type>,System.Collections.Generic.List`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCase>,System.Int32&)
BuildPocoObjectCase(System.Type,System.Text.Json.Serialization.Metadata.JsonTypeInfo,System.Text.Json.JsonSerializerOptions,System.Int32,System.Int32&)
ValidatePocoObjectCases(System.Type,System.Collections.Generic.List`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCase>)
.ctor(System.Collections.Generic.Dictionary`2<System.Text.Json.Serialization.JsonValueType,System.Type>,System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCase[],System.Collections.Generic.Dictionary`2<System.Byte[],System.Collections.Generic.List`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoPropertyClassifierInfo>>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoPropertyClassifierInfo>>,System.Int32)
Classify(System.Text.Json.Utf8JsonReader&)
ClassifyJsonObject(System.Text.Json.Utf8JsonReader&)
SelectJsonObjectCase(System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCase[],System.ReadOnlySpan`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCaseState>,System.Text.Json.Serialization.ValueBitArray)
ReadNextProperty(System.Text.Json.Utf8JsonReader&,System.Int32,System.Span`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCaseState>,System.Text.Json.Serialization.ValueBitArray,System.Text.Json.Serialization.ValueBitArray,System.Text.Json.Serialization.ValueBitArray)
RecordPropertyMatches(System.ReadOnlySpan`1<System.Byte>,System.Int32,System.Span`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCaseState>,System.Text.Json.Serialization.ValueBitArray,System.Text.Json.Serialization.ValueBitArray,System.Text.Json.Serialization.ValueBitArray)
RecordPropertyMatch(System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoPropertyClassifierInfo,System.Int32,System.Span`1<System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCaseState>,System.Text.Json.Serialization.ValueBitArray,System.Text.Json.Serialization.ValueBitArray,System.Text.Json.Serialization.ValueBitArray)
.ctor(System.Type,System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoPropertyClassifierInfo[],System.Boolean,System.Int32)
CaseType()
DisallowUnmappedProperties()
Properties()
RequiredCount()
IsShadowedBy(System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoObjectCase)
.ctor(System.String,System.Boolean,System.Boolean,System.Int32,System.Int32)
PocoCaseIndex()
IsCaseSensitive()
IsRequired()
Name()
NameAsUtf8Bytes()
RequiredPropertyIndex()
IsPropertyNameEquivalent(System.Text.Json.Serialization.JsonUnionTypeStructuralClassifier/PocoPropertyClassifierInfo)
LastMatchedPropertyIndex()
LastMatchedPropertyIndex(System.Int32)