| File: System\Text\Json\Serialization\Metadata\PolymorphicTypeResolver.cs | Web Access |
| Project: src\runtime\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace System.Text.Json.Serialization.Metadata { /// <summary> /// Validates and indexes polymorphic type configuration, /// providing derived JsonTypeInfo resolution methods /// in both serialization and deserialization scenaria. /// </summary> internal sealed class PolymorphicTypeResolver { private readonly ConcurrentDictionary<Type, DerivedJsonTypeInfo?> _typeToDiscriminatorId = new(); private readonly Dictionary<object, DerivedJsonTypeInfo>? _discriminatorIdtoType; private readonly JsonSerializerOptions _options; public PolymorphicTypeResolver(JsonSerializerOptions options, JsonPolymorphismOptions polymorphismOptions, Type baseType, bool converterCanHaveMetadata) { UnknownDerivedTypeHandling = polymorphismOptions.UnknownDerivedTypeHandling; IgnoreUnrecognizedTypeDiscriminators = polymorphismOptions.IgnoreUnrecognizedTypeDiscriminators; BaseType = baseType; _options = options; if (!IsSupportedPolymorphicBaseType(BaseType)) { ThrowHelper.ThrowInvalidOperationException_TypeDoesNotSupportPolymorphism(BaseType); } bool containsDerivedTypes = false; foreach ((Type derivedType, object? typeDiscriminator) in polymorphismOptions.DerivedTypes) { Debug.Assert(typeDiscriminator is null or int or string); if (!IsSupportedDerivedType(BaseType, derivedType) || (derivedType.IsAbstract && UnknownDerivedTypeHandling != JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)) { ThrowHelper.ThrowInvalidOperationException_DerivedTypeNotSupported(BaseType, derivedType); } JsonTypeInfo derivedTypeInfo = options.GetTypeInfoInternal(derivedType); DerivedJsonTypeInfo derivedTypeInfoHolder = new(typeDiscriminator, derivedTypeInfo); if (!_typeToDiscriminatorId.TryAdd(derivedType, derivedTypeInfoHolder)) { ThrowHelper.ThrowInvalidOperationException_DerivedTypeIsAlreadySpecified(BaseType, derivedType); } if (typeDiscriminator is not null) { if (!(_discriminatorIdtoType ??= new()).TryAdd(typeDiscriminator, derivedTypeInfoHolder)) { ThrowHelper.ThrowInvalidOperationException_TypeDicriminatorIdIsAlreadySpecified(BaseType, typeDiscriminator); } UsesTypeDiscriminators = true; } containsDerivedTypes = true; } if (!containsDerivedTypes) { ThrowHelper.ThrowInvalidOperationException_PolymorphicTypeConfigurationDoesNotSpecifyDerivedTypes(BaseType); } if (UsesTypeDiscriminators) { Debug.Assert(_discriminatorIdtoType != null, "Discriminator index must have been populated."); if (!converterCanHaveMetadata) { ThrowHelper.ThrowNotSupportedException_BaseConverterDoesNotSupportMetadata(BaseType); } string propertyName = polymorphismOptions.TypeDiscriminatorPropertyName; if (!propertyName.Equals(JsonSerializer.TypePropertyName, StringComparison.Ordinal)) { byte[] utf8EncodedName = Encoding.UTF8.GetBytes(propertyName); // Check if the property name conflicts with other metadata property names if ((JsonSerializer.GetMetadataPropertyName(utf8EncodedName, resolver: null) & ~MetadataPropertyName.Type) != 0) { ThrowHelper.ThrowInvalidOperationException_InvalidCustomTypeDiscriminatorPropertyName(); } CustomTypeDiscriminatorPropertyNameUtf8 = utf8EncodedName; CustomTypeDiscriminatorPropertyNameJsonEncoded = JsonEncodedText.Encode(propertyName, options.Encoder); } // Check if the discriminator property name conflicts with any derived property names. foreach (DerivedJsonTypeInfo derivedTypeInfo in _discriminatorIdtoType.Values) { if (derivedTypeInfo.JsonTypeInfo.Kind is JsonTypeInfoKind.Object) { foreach (JsonPropertyInfo property in derivedTypeInfo.JsonTypeInfo.Properties) { if (property is { IsIgnored: false, IsExtensionData: false } && property.Name == propertyName) { ThrowHelper.ThrowInvalidOperationException_PropertyConflictsWithMetadataPropertyName(derivedTypeInfo.JsonTypeInfo.Type, propertyName); } } } } } } public Type BaseType { get; } public JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; } public bool UsesTypeDiscriminators { get; } public bool IgnoreUnrecognizedTypeDiscriminators { get; } public byte[]? CustomTypeDiscriminatorPropertyNameUtf8 { get; } public JsonEncodedText? CustomTypeDiscriminatorPropertyNameJsonEncoded { get; } /// <summary> /// Resolves a classifier-returned <see cref="Type"/> to its <see cref="JsonTypeInfo"/> using the registered derived types. /// </summary> public bool TryResolveDerivedJsonTypeInfo(Type resolvedType, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo) { if (_typeToDiscriminatorId.TryGetValue(resolvedType, out DerivedJsonTypeInfo? result) && result is not null) { jsonTypeInfo = result.JsonTypeInfo; return true; } if (IgnoreUnrecognizedTypeDiscriminators) { jsonTypeInfo = null; return false; } ThrowHelper.ThrowNotSupportedException_RuntimeTypeNotSupported(BaseType, resolvedType); jsonTypeInfo = null; return false; } public bool TryGetDerivedJsonTypeInfo(Type runtimeType, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo, out object? typeDiscriminator) { Debug.Assert(BaseType.IsAssignableFrom(runtimeType)); if (!_typeToDiscriminatorId.TryGetValue(runtimeType, out DerivedJsonTypeInfo? result)) { switch (UnknownDerivedTypeHandling) { case JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor: // Calculate (and cache the result) of the nearest ancestor for given runtime type. // A `null` result denotes no matching ancestor type, we also cache that. result = CalculateNearestAncestor(runtimeType); _typeToDiscriminatorId[runtimeType] = result; break; case JsonUnknownDerivedTypeHandling.FallBackToBaseType: // Recover the polymorphic contract (i.e. any type discriminators) for the base type, if it exists. _typeToDiscriminatorId.TryGetValue(BaseType, out result); _typeToDiscriminatorId[runtimeType] = result; break; case JsonUnknownDerivedTypeHandling.FailSerialization: default: if (runtimeType != BaseType) { ThrowHelper.ThrowNotSupportedException_RuntimeTypeNotSupported(BaseType, runtimeType); } break; } } if (result is null) { jsonTypeInfo = null; typeDiscriminator = null; return false; } else { jsonTypeInfo = result.JsonTypeInfo; typeDiscriminator = result.TypeDiscriminator; return true; } } public bool TryGetDerivedJsonTypeInfo(object typeDiscriminator, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo) { Debug.Assert(typeDiscriminator is int or string); Debug.Assert(UsesTypeDiscriminators); Debug.Assert(_discriminatorIdtoType != null); if (_discriminatorIdtoType.TryGetValue(typeDiscriminator, out DerivedJsonTypeInfo? result)) { Debug.Assert(typeDiscriminator.Equals(result.TypeDiscriminator)); jsonTypeInfo = result.JsonTypeInfo; return true; } if (!IgnoreUnrecognizedTypeDiscriminators) { ThrowHelper.ThrowJsonException_UnrecognizedTypeDiscriminator(typeDiscriminator); } jsonTypeInfo = null; return false; } public static bool IsSupportedPolymorphicBaseType(Type? type) => type != null && (type.IsClass || type.IsInterface) && !type.IsSealed && !type.IsGenericTypeDefinition && !type.IsPointer && type != JsonTypeInfo.ObjectType; public static bool IsSupportedDerivedType(Type baseType, Type? derivedType) => baseType.IsAssignableFrom(derivedType) && !derivedType.IsGenericTypeDefinition; [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", Justification = "The call to GetInterfaces will cross-reference results with interface types " + "already declared as derived types of the polymorphic base type.")] private DerivedJsonTypeInfo? CalculateNearestAncestor(Type type) { Debug.Assert(!type.IsAbstract); Debug.Assert(BaseType.IsAssignableFrom(type)); Debug.Assert(UnknownDerivedTypeHandling == JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor); if (type == BaseType) { return null; } DerivedJsonTypeInfo? result = null; // First, walk up the class hierarchy for any supported types. for (Type? candidate = type.BaseType; BaseType.IsAssignableFrom(candidate); candidate = candidate.BaseType) { Debug.Assert(candidate != null); if (_typeToDiscriminatorId.TryGetValue(candidate, out result)) { break; } } // Interface hierarchies admit the possibility of diamond ambiguities in type discriminators. // Examine all interface implementations and identify potential conflicts. if (BaseType.IsInterface) { foreach (Type interfaceTy in type.GetInterfaces()) { if (interfaceTy != BaseType && BaseType.IsAssignableFrom(interfaceTy) && _typeToDiscriminatorId.TryGetValue(interfaceTy, out DerivedJsonTypeInfo? interfaceResult) && interfaceResult is not null) { if (result is null) { result = interfaceResult; } else { ThrowHelper.ThrowNotSupportedException_RuntimeTypeDiamondAmbiguity(BaseType, type, result.JsonTypeInfo.Type, interfaceResult.JsonTypeInfo.Type); } } } } return result; } /// <summary> /// Walks the type hierarchy above the current type for any types that use polymorphic configuration. /// </summary> [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "The call to GetInterfaces will cross-reference results with interface types " + "already declared as derived types of the polymorphic base type.")] internal static JsonTypeInfo? FindNearestPolymorphicBaseType(JsonTypeInfo typeInfo) { Debug.Assert(typeInfo.IsConfigured); if (typeInfo.PolymorphismOptions != null) { // Type defines its own polymorphic configuration. return null; } JsonTypeInfo? matchingResult = null; // First, walk up the class hierarchy for any supported types. for (Type? candidate = typeInfo.Type.BaseType; candidate != null; candidate = candidate.BaseType) { JsonTypeInfo? candidateInfo = ResolveAncestorTypeInfo(candidate, typeInfo.Options); if (candidateInfo?.PolymorphismOptions != null) { // stop on the first ancestor that has a match matchingResult = candidateInfo; break; } } // Now, walk the interface hierarchy for any polymorphic interface declarations. foreach (Type interfaceType in typeInfo.Type.GetInterfaces()) { JsonTypeInfo? candidateInfo = ResolveAncestorTypeInfo(interfaceType, typeInfo.Options); if (candidateInfo?.PolymorphismOptions != null) { if (matchingResult != null) { // Resolve any conflicting matches. if (matchingResult.Type.IsAssignableFrom(interfaceType)) { // interface is more derived than previous match, replace it. matchingResult = candidateInfo; } else if (interfaceType.IsAssignableFrom(matchingResult.Type)) { // interface is less derived than previous match, keep the previous one. continue; } else { // Diamond ambiguity, do not report any ancestors. return null; } } else { matchingResult = candidateInfo; } } } return matchingResult; static JsonTypeInfo? ResolveAncestorTypeInfo(Type type, JsonSerializerOptions options) { try { return options.GetTypeInfoInternal(type, ensureNotNull: null); } catch { // The resolver produced an exception when resolving the ancestor type. // Eat the exception and report no result instead. return null; } } } /// <summary> /// JsonTypeInfo result holder for a derived type. /// </summary> private sealed class DerivedJsonTypeInfo { public DerivedJsonTypeInfo(object? typeDiscriminator, JsonTypeInfo derivedTypeInfo) { Debug.Assert(typeDiscriminator is null or int or string); TypeDiscriminator = typeDiscriminator; JsonTypeInfo = derivedTypeInfo; } public object? TypeDiscriminator { get; } public JsonTypeInfo JsonTypeInfo { get; } } } }