< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 217
Coverable lines: 217
Total lines: 367
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 126
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/Metadata/PolymorphicTypeResolver.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.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Diagnostics.CodeAnalysis;
 8
 9namespace System.Text.Json.Serialization.Metadata
 10{
 11    /// <summary>
 12    /// Validates and indexes polymorphic type configuration,
 13    /// providing derived JsonTypeInfo resolution methods
 14    /// in both serialization and deserialization scenaria.
 15    /// </summary>
 16    internal sealed class PolymorphicTypeResolver
 17    {
 018        private readonly ConcurrentDictionary<Type, DerivedJsonTypeInfo?> _typeToDiscriminatorId = new();
 19        private readonly Dictionary<object, DerivedJsonTypeInfo>? _discriminatorIdtoType;
 20        private readonly JsonSerializerOptions _options;
 21
 022        public PolymorphicTypeResolver(JsonSerializerOptions options, JsonPolymorphismOptions polymorphismOptions, Type 
 023        {
 024            UnknownDerivedTypeHandling = polymorphismOptions.UnknownDerivedTypeHandling;
 025            IgnoreUnrecognizedTypeDiscriminators = polymorphismOptions.IgnoreUnrecognizedTypeDiscriminators;
 026            BaseType = baseType;
 027            _options = options;
 28
 029            if (!IsSupportedPolymorphicBaseType(BaseType))
 030            {
 031                ThrowHelper.ThrowInvalidOperationException_TypeDoesNotSupportPolymorphism(BaseType);
 32            }
 33
 034            bool containsDerivedTypes = false;
 035            foreach ((Type derivedType, object? typeDiscriminator) in polymorphismOptions.DerivedTypes)
 036            {
 037                Debug.Assert(typeDiscriminator is null or int or string);
 38
 039                if (!IsSupportedDerivedType(BaseType, derivedType) ||
 040                    (derivedType.IsAbstract && UnknownDerivedTypeHandling != JsonUnknownDerivedTypeHandling.FallBackToNe
 041                {
 042                    ThrowHelper.ThrowInvalidOperationException_DerivedTypeNotSupported(BaseType, derivedType);
 43                }
 44
 045                JsonTypeInfo derivedTypeInfo = options.GetTypeInfoInternal(derivedType);
 046                DerivedJsonTypeInfo derivedTypeInfoHolder = new(typeDiscriminator, derivedTypeInfo);
 47
 048                if (!_typeToDiscriminatorId.TryAdd(derivedType, derivedTypeInfoHolder))
 049                {
 050                    ThrowHelper.ThrowInvalidOperationException_DerivedTypeIsAlreadySpecified(BaseType, derivedType);
 51                }
 52
 053                if (typeDiscriminator is not null)
 054                {
 055                    if (!(_discriminatorIdtoType ??= new()).TryAdd(typeDiscriminator, derivedTypeInfoHolder))
 056                    {
 057                        ThrowHelper.ThrowInvalidOperationException_TypeDicriminatorIdIsAlreadySpecified(BaseType, typeDi
 58                    }
 59
 060                    UsesTypeDiscriminators = true;
 061                }
 62
 063                containsDerivedTypes = true;
 064            }
 65
 066            if (!containsDerivedTypes)
 067            {
 068                ThrowHelper.ThrowInvalidOperationException_PolymorphicTypeConfigurationDoesNotSpecifyDerivedTypes(BaseTy
 69            }
 70
 071            if (UsesTypeDiscriminators)
 072            {
 073                Debug.Assert(_discriminatorIdtoType is not null, "Discriminator index must have been populated.");
 74
 075                if (!converterCanHaveMetadata)
 076                {
 077                    ThrowHelper.ThrowNotSupportedException_BaseConverterDoesNotSupportMetadata(BaseType);
 78                }
 79
 080                string propertyName = polymorphismOptions.TypeDiscriminatorPropertyName;
 081                if (!propertyName.Equals(JsonSerializer.TypePropertyName, StringComparison.Ordinal))
 082                {
 083                    byte[] utf8EncodedName = Encoding.UTF8.GetBytes(propertyName);
 84
 85                    // Check if the property name conflicts with other metadata property names
 086                    if ((JsonSerializer.GetMetadataPropertyName(utf8EncodedName, resolver: null) & ~MetadataPropertyName
 087                    {
 088                        ThrowHelper.ThrowInvalidOperationException_InvalidCustomTypeDiscriminatorPropertyName();
 89                    }
 90
 091                    CustomTypeDiscriminatorPropertyNameUtf8 = utf8EncodedName;
 092                    CustomTypeDiscriminatorPropertyNameJsonEncoded = JsonEncodedText.Encode(propertyName, options.Encode
 093                }
 94
 95                // Check if the discriminator property name conflicts with any derived property names.
 096                foreach (DerivedJsonTypeInfo derivedTypeInfo in _discriminatorIdtoType.Values)
 097                {
 098                    if (derivedTypeInfo.JsonTypeInfo.Kind is JsonTypeInfoKind.Object)
 099                    {
 0100                        foreach (JsonPropertyInfo property in derivedTypeInfo.JsonTypeInfo.Properties)
 0101                        {
 0102                            if (property is { IsIgnored: false, IsExtensionData: false } && property.Name == propertyNam
 0103                            {
 0104                                ThrowHelper.ThrowInvalidOperationException_PropertyConflictsWithMetadataPropertyName(der
 105                            }
 0106                        }
 0107                    }
 0108                }
 0109            }
 0110        }
 111
 0112        public Type BaseType { get; }
 0113        public JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; }
 0114        public bool UsesTypeDiscriminators { get; }
 0115        public bool IgnoreUnrecognizedTypeDiscriminators { get; }
 0116        public byte[]? CustomTypeDiscriminatorPropertyNameUtf8 { get; }
 0117        public JsonEncodedText? CustomTypeDiscriminatorPropertyNameJsonEncoded { get; }
 118
 119        /// <summary>
 120        /// Resolves a classifier-returned <see cref="Type"/> to its <see cref="JsonTypeInfo"/> using the registered der
 121        /// </summary>
 122        public bool TryResolveDerivedJsonTypeInfo(Type resolvedType, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo)
 0123        {
 0124            if (_typeToDiscriminatorId.TryGetValue(resolvedType, out DerivedJsonTypeInfo? result) && result is not null)
 0125            {
 0126                jsonTypeInfo = result.JsonTypeInfo;
 0127                return true;
 128            }
 129
 0130            if (IgnoreUnrecognizedTypeDiscriminators)
 0131            {
 0132                jsonTypeInfo = null;
 0133                return false;
 134            }
 135
 0136            ThrowHelper.ThrowNotSupportedException_RuntimeTypeNotSupported(BaseType, resolvedType);
 137            jsonTypeInfo = null;
 138            return false;
 0139        }
 140
 141        public bool TryGetDerivedJsonTypeInfo(Type runtimeType, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo, out 
 0142        {
 0143            Debug.Assert(BaseType.IsAssignableFrom(runtimeType));
 144
 0145            if (!_typeToDiscriminatorId.TryGetValue(runtimeType, out DerivedJsonTypeInfo? result))
 0146            {
 0147                switch (UnknownDerivedTypeHandling)
 148                {
 149                    case JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor:
 150                        // Calculate (and cache the result) of the nearest ancestor for given runtime type.
 151                        // A `null` result denotes no matching ancestor type, we also cache that.
 0152                        result = CalculateNearestAncestor(runtimeType);
 0153                        _typeToDiscriminatorId[runtimeType] = result;
 0154                        break;
 155                    case JsonUnknownDerivedTypeHandling.FallBackToBaseType:
 156                        // Recover the polymorphic contract (i.e. any type discriminators) for the base type, if it exis
 0157                        _typeToDiscriminatorId.TryGetValue(BaseType, out result);
 0158                        _typeToDiscriminatorId[runtimeType] = result;
 0159                        break;
 160
 161                    case JsonUnknownDerivedTypeHandling.FailSerialization:
 162                    default:
 0163                        if (runtimeType != BaseType)
 0164                        {
 0165                            ThrowHelper.ThrowNotSupportedException_RuntimeTypeNotSupported(BaseType, runtimeType);
 166                        }
 0167                        break;
 168                }
 0169            }
 170
 0171            if (result is null)
 0172            {
 0173                jsonTypeInfo = null;
 0174                typeDiscriminator = null;
 0175                return false;
 176            }
 177            else
 0178            {
 0179                jsonTypeInfo = result.JsonTypeInfo;
 0180                typeDiscriminator = result.TypeDiscriminator;
 0181                return true;
 182            }
 0183        }
 184
 185        public bool TryGetDerivedJsonTypeInfo(object typeDiscriminator, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeIn
 0186        {
 0187            Debug.Assert(typeDiscriminator is int or string);
 0188            Debug.Assert(UsesTypeDiscriminators);
 0189            Debug.Assert(_discriminatorIdtoType is not null);
 190
 0191            if (_discriminatorIdtoType.TryGetValue(typeDiscriminator, out DerivedJsonTypeInfo? result))
 0192            {
 0193                Debug.Assert(typeDiscriminator.Equals(result.TypeDiscriminator));
 0194                jsonTypeInfo = result.JsonTypeInfo;
 0195                return true;
 196            }
 197
 0198            if (!IgnoreUnrecognizedTypeDiscriminators)
 0199            {
 0200                ThrowHelper.ThrowJsonException_UnrecognizedTypeDiscriminator(typeDiscriminator);
 201            }
 202
 0203            jsonTypeInfo = null;
 0204            return false;
 0205        }
 206
 207        public static bool IsSupportedPolymorphicBaseType(Type? type) =>
 0208            type != null &&
 0209            (type.IsClass || type.IsInterface) &&
 0210            !type.IsSealed &&
 0211            !type.IsGenericTypeDefinition &&
 0212            !type.IsPointer &&
 0213            type != typeof(object);
 214
 215        public static bool IsSupportedDerivedType(Type baseType, Type? derivedType) =>
 0216            baseType.IsAssignableFrom(derivedType) && !derivedType.IsGenericTypeDefinition;
 217
 218        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
 219            Justification = "The call to GetInterfaces will cross-reference results with interface types " +
 220                            "already declared as derived types of the polymorphic base type.")]
 221        private DerivedJsonTypeInfo? CalculateNearestAncestor(Type type)
 0222        {
 0223            Debug.Assert(!type.IsAbstract);
 0224            Debug.Assert(BaseType.IsAssignableFrom(type));
 0225            Debug.Assert(UnknownDerivedTypeHandling == JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor);
 226
 0227            if (type == BaseType)
 0228            {
 0229                return null;
 230            }
 231
 0232            DerivedJsonTypeInfo? result = null;
 233
 234            // First, walk up the class hierarchy for any supported types.
 0235            for (Type? candidate = type.BaseType; BaseType.IsAssignableFrom(candidate); candidate = candidate.BaseType)
 0236            {
 0237                Debug.Assert(candidate != null);
 238
 0239                if (_typeToDiscriminatorId.TryGetValue(candidate, out result))
 0240                {
 0241                    break;
 242                }
 0243            }
 244
 245            // Interface hierarchies admit the possibility of diamond ambiguities in type discriminators.
 246            // Examine all interface implementations and identify potential conflicts.
 0247            if (BaseType.IsInterface)
 0248            {
 0249                foreach (Type interfaceTy in type.GetInterfaces())
 0250                {
 0251                    if (interfaceTy != BaseType && BaseType.IsAssignableFrom(interfaceTy) &&
 0252                        _typeToDiscriminatorId.TryGetValue(interfaceTy, out DerivedJsonTypeInfo? interfaceResult) &&
 0253                        interfaceResult is not null)
 0254                    {
 0255                        if (result is null)
 0256                        {
 0257                            result = interfaceResult;
 0258                        }
 259                        else
 0260                        {
 0261                            ThrowHelper.ThrowNotSupportedException_RuntimeTypeDiamondAmbiguity(BaseType, type, result.Js
 262                        }
 0263                    }
 0264                }
 0265            }
 266
 0267            return result;
 0268        }
 269
 270        /// <summary>
 271        /// Walks the type hierarchy above the current type for any types that use polymorphic configuration.
 272        /// </summary>
 273        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern",
 274            Justification = "The call to GetInterfaces will cross-reference results with interface types " +
 275                            "already declared as derived types of the polymorphic base type.")]
 276        internal static JsonTypeInfo? FindNearestPolymorphicBaseType(JsonTypeInfo typeInfo)
 0277        {
 0278            Debug.Assert(typeInfo.IsConfigured);
 279
 0280            if (typeInfo.PolymorphismOptions is not null)
 0281            {
 282                // Type defines its own polymorphic configuration.
 0283                return null;
 284            }
 285
 0286            JsonTypeInfo? matchingResult = null;
 287
 288            // First, walk up the class hierarchy for any supported types.
 0289            for (Type? candidate = typeInfo.Type.BaseType; candidate != null; candidate = candidate.BaseType)
 0290            {
 0291                JsonTypeInfo? candidateInfo = ResolveAncestorTypeInfo(candidate, typeInfo.Options);
 0292                if (candidateInfo?.PolymorphismOptions is not null)
 0293                {
 294                    // stop on the first ancestor that has a match
 0295                    matchingResult = candidateInfo;
 0296                    break;
 297                }
 0298            }
 299
 300            // Now, walk the interface hierarchy for any polymorphic interface declarations.
 0301            foreach (Type interfaceType in typeInfo.Type.GetInterfaces())
 0302            {
 0303                JsonTypeInfo? candidateInfo = ResolveAncestorTypeInfo(interfaceType, typeInfo.Options);
 0304                if (candidateInfo?.PolymorphismOptions is not null)
 0305                {
 0306                    if (matchingResult is not null)
 0307                    {
 308                        // Resolve any conflicting matches.
 0309                        if (matchingResult.Type.IsAssignableFrom(interfaceType))
 0310                        {
 311                            // interface is more derived than previous match, replace it.
 0312                            matchingResult = candidateInfo;
 0313                        }
 0314                        else if (interfaceType.IsAssignableFrom(matchingResult.Type))
 0315                        {
 316                            // interface is less derived than previous match, keep the previous one.
 0317                            continue;
 318                        }
 319                        else
 0320                        {
 321                            // Diamond ambiguity, do not report any ancestors.
 0322                            return null;
 323                        }
 0324                    }
 325                    else
 0326                    {
 0327                        matchingResult = candidateInfo;
 0328                    }
 0329                }
 0330            }
 331
 0332            return matchingResult;
 333
 334            static JsonTypeInfo? ResolveAncestorTypeInfo(Type type, JsonSerializerOptions options)
 0335            {
 336                try
 0337                {
 0338                    return options.GetTypeInfoInternal(type, ensureNotNull: null);
 339                }
 0340                catch
 0341                {
 342                    // The resolver produced an exception when resolving the ancestor type.
 343                    // Eat the exception and report no result instead.
 0344                    return null;
 345                }
 0346            }
 0347        }
 348
 349        /// <summary>
 350        /// JsonTypeInfo result holder for a derived type.
 351        /// </summary>
 352        private sealed class DerivedJsonTypeInfo
 353        {
 0354            public DerivedJsonTypeInfo(object? typeDiscriminator, JsonTypeInfo derivedTypeInfo)
 0355            {
 0356                Debug.Assert(typeDiscriminator is null or int or string);
 357
 0358                TypeDiscriminator = typeDiscriminator;
 0359                JsonTypeInfo = derivedTypeInfo;
 0360            }
 361
 0362            public object? TypeDiscriminator { get; }
 0363            public JsonTypeInfo JsonTypeInfo { get; }
 364        }
 365    }
 366}
 367