From e88fa950aab6526f02abe075d1bd93c6db7a41b5 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 29 Jun 2026 14:56:04 +0300 Subject: [PATCH 1/9] Add InferClosedTypePolymorphism for closed-hierarchy polymorphism inference Implements feature #129041: a global InferClosedTypePolymorphism flag on JsonSerializerOptions and JsonSourceGenerationOptionsAttribute that infers polymorphic \ discriminators from compiler-emitted [IsClosedType] metadata when no explicit [JsonDerivedType] set is present. - Reflection: DefaultJsonTypeInfoResolver infers one derived type per closed hierarchy member; throws for inaccessible/un-unifiable derived types. - Source-gen: forward-compat parser plumbing mirrors reflection parity (throwing factory for inaccessible/open-generic, kept entry for unassignable). - Wire flag into options copy-ctor and cache equality comparer. - New diagnostics SYSLIB1240-1242 for derived-type validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/project/list-of-diagnostics.md | 10 + .../JsonSourceGenerationOptionsAttribute.cs | 5 + .../gen/Helpers/KnownTypeSymbols.cs | 3 + ...onSourceGenerator.DiagnosticDescriptors.cs | 24 ++ .../gen/JsonSourceGenerator.Emitter.cs | 65 +++--- .../gen/JsonSourceGenerator.Parser.cs | 182 ++++++++++++++- .../gen/Model/PolymorphismOptionsSpec.cs | 10 + .../gen/Model/SourceGenerationOptionsSpec.cs | 2 + .../gen/Resources/Strings.resx | 18 ++ .../gen/Resources/xlf/Strings.cs.xlf | 30 +++ .../gen/Resources/xlf/Strings.de.xlf | 30 +++ .../gen/Resources/xlf/Strings.es.xlf | 30 +++ .../gen/Resources/xlf/Strings.fr.xlf | 30 +++ .../gen/Resources/xlf/Strings.it.xlf | 30 +++ .../gen/Resources/xlf/Strings.ja.xlf | 30 +++ .../gen/Resources/xlf/Strings.ko.xlf | 30 +++ .../gen/Resources/xlf/Strings.pl.xlf | 30 +++ .../gen/Resources/xlf/Strings.pt-BR.xlf | 30 +++ .../gen/Resources/xlf/Strings.ru.xlf | 30 +++ .../gen/Resources/xlf/Strings.tr.xlf | 30 +++ .../gen/Resources/xlf/Strings.zh-Hans.xlf | 30 +++ .../gen/Resources/xlf/Strings.zh-Hant.xlf | 30 +++ .../System.Text.Json/ref/System.Text.Json.cs | 2 + .../src/Resources/Strings.resx | 3 + .../JsonSerializerOptions.Caching.cs | 2 + .../Serialization/JsonSerializerOptions.cs | 25 ++ .../DefaultJsonTypeInfoResolver.Helpers.cs | 59 +++++ .../Text/Json/ThrowHelper.Serialization.cs | 6 + .../JsonCreationHandlingTests.Object.cs | 2 + .../PolymorphicTests.ClosedTypeInference.cs | 95 ++++++++ .../PolymorphicTests.CustomTypeHierarchies.cs | 6 + .../ClosedTypeTestFixtures.il | 213 ++++++++++++++++++ ...em.Text.Json.ClosedTypeTestFixtures.ilproj | 13 ++ .../Serialization/CacheTests.cs | 1 + .../System.Text.Json.Tests.csproj | 6 + 35 files changed, 1105 insertions(+), 37 deletions(-) create mode 100644 src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index 9f312d97c5aa18..aa7ddbcfec1739 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -286,6 +286,16 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL | __`SYSLIB1237`__ | _`SYSLIB1230`-`SYSLIB1239` reserved for Microsoft.Interop.ComInterfaceGenerator._ | | __`SYSLIB1238`__ | _`SYSLIB1230`-`SYSLIB1239` reserved for Microsoft.Interop.ComInterfaceGenerator._ | | __`SYSLIB1239`__ | _`SYSLIB1230`-`SYSLIB1239` reserved for Microsoft.Interop.ComInterfaceGenerator._ | +| __`SYSLIB1240`__ | Derived type is not a supported polymorphic derived type. | +| __`SYSLIB1241`__ | Inferred derived type is less accessible than the polymorphic base type. | +| __`SYSLIB1242`__ | Inferred derived types produce a duplicate type discriminator. | +| __`SYSLIB1243`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1244`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1245`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1246`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1247`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1248`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1249`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | ### Diagnostic Suppressions (`SYSLIBSUPPRESS****`) diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs index dab19ca6d0e724..31866272e5cb9d 100644 --- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs @@ -187,5 +187,10 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults) /// Specifies the default value of when set. /// public bool AllowDuplicateProperties { get; set; } + + /// + /// Specifies the default value of when set. + /// + public bool InferClosedTypePolymorphism { get; set; } } } diff --git a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs index 5b7d2c5d21f76f..1e18dd675315d9 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs @@ -261,6 +261,9 @@ public KnownTypeSymbols(Compilation compilation) public INamedTypeSymbol? UnsafeAccessorAttributeType => GetOrResolveType("System.Runtime.CompilerServices.UnsafeAccessorAttribute", ref _UnsafeAccessorAttributeType); private Option _UnsafeAccessorAttributeType; + public INamedTypeSymbol? IsClosedTypeAttributeType => GetOrResolveType("System.Runtime.CompilerServices.IsClosedTypeAttribute", ref _IsClosedTypeAttributeType); + private Option _IsClosedTypeAttributeType; + // OverloadResolutionPriorityAttribute was added in .NET 9; its presence indicates // the runtime also supports generic type parameters in UnsafeAccessor. public bool SupportsGenericUnsafeAccessors => UnsafeAccessorAttributeType is not null diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs index 5be4c1ff76d97f..fefe33a1edfae0 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs @@ -147,6 +147,30 @@ internal static class DiagnosticDescriptors category: JsonConstants.SystemTextJsonSourceGenerationName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static DiagnosticDescriptor DerivedTypeIsNotSupported { get; } = DiagnosticDescriptorHelper.Create( + id: "SYSLIB1240", + title: new LocalizableResourceString(nameof(SR.DerivedTypeIsNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.DerivedTypeIsNotSupportedMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: JsonConstants.SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static DiagnosticDescriptor InferredDerivedTypeIsNotAccessible { get; } = DiagnosticDescriptorHelper.Create( + id: "SYSLIB1241", + title: new LocalizableResourceString(nameof(SR.InferredDerivedTypeIsNotAccessibleTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.InferredDerivedTypeIsNotAccessibleMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: JsonConstants.SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static DiagnosticDescriptor InferredDerivedTypeDiscriminatorCollision { get; } = DiagnosticDescriptorHelper.Create( + id: "SYSLIB1242", + title: new LocalizableResourceString(nameof(SR.InferredDerivedTypeDiscriminatorCollisionTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.InferredDerivedTypeDiscriminatorCollisionMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: JsonConstants.SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 7b50edd6bc6857..4c50be8edcca88 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -242,6 +242,11 @@ private static SourceText CompleteSourceFileAndReturnText(SourceWriter writer) private SourceText? GenerateTypeInfo(ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) { + if (typeGenerationSpec.PolymorphismOptions?.UnresolvedDerivedTypeError is not null) + { + return GenerateForTypeWithUnresolvedPolymorphism(contextSpec, typeGenerationSpec); + } + switch (typeGenerationSpec.ClassType) { case ClassType.BuiltInSupportType: @@ -353,6 +358,27 @@ private static SourceText GenerateForUnsupportedType(ContextGenerationSpec conte return CompleteSourceFileAndReturnText(writer); } + private static SourceText GenerateForTypeWithUnresolvedPolymorphism(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) + { + Debug.Assert(typeMetadata.PolymorphismOptions?.UnresolvedDerivedTypeError is not null); + + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); + + GenerateTypeInfoFactoryHeader(writer, typeMetadata); + + // A registered derived type could not be resolved against this base type. Emit a + // factory body that throws the same way the reflection-based resolver does, rather + // than silently producing non-polymorphic metadata. The throw is the only statement + // inside the 'if (!TryGetTypeInfoForRuntimeCustomConverter(...))' block, so the + // normal 'return jsonTypeInfo' path (taken when a runtime custom converter applies) + // remains reachable and the method still compiles. + writer.WriteLine($"""throw new {InvalidOperationExceptionTypeRef}("{typeMetadata.PolymorphismOptions!.UnresolvedDerivedTypeError}");"""); + + GenerateTypeInfoFactoryFooter(writer); + + return CompleteSourceFileAndReturnText(writer); + } + private static SourceText GenerateForEnum(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec, experimentalDiagnosticIds: typeMetadata.ExperimentalDiagnosticIds); @@ -686,20 +712,6 @@ private static SourceText GenerateForUnion(ContextGenerationSpec contextSpec, Ty // the canonical switch arm (preferring the non-Nullable sibling so // most-derived dispatch reports typeof(T)). The null payload is handled // separately by the `null =>` arm via nullCase. - int switchArmCount = 0; - bool armsMergeDeclaredCases = false; - foreach (UnionCaseSpec caseSpec in unionCases) - { - if (caseSpec.IsSwitchArm) - { - switchArmCount++; - } - else - { - armsMergeDeclaredCases = true; - } - } - string unionCasesExpr = unionCases.Count == 0 ? $"global::System.Array.Empty<{JsonUnionCaseInfoTypeRef}>()" : $$"""new {{JsonUnionCaseInfoTypeRef}}[] { {{string.Join(", ", unionCases.Select(c => $"new {JsonUnionCaseInfoTypeRef}(typeof({c.CaseType.FullyQualifiedName})) {{ IsNullable = {(c.IsNullable ? "true" : "false")} }}"))}} }"""; @@ -748,22 +760,7 @@ private static SourceText GenerateForUnion(ContextGenerationSpec contextSpec, Ty writer.WriteLine("},"); // The deconstructor switch has no `_` arm — it relies on the union's - // declared case set being exhaustively covered by its arms. Roslyn's - // union exhaustiveness analyzer fails to recognize coverage in two - // shapes today: (a) when switchArmCount == 1 the switch looks - // non-exhaustive on `object?`-shaped surface area, and (b) when - // Foo(T)+Foo(Nullable) overloads merge into a single `T` arm the - // Nullable declared case isn't seen as covered. Tracked by - // https://github.com/dotnet/roslyn/issues/83666; the fix is present - // in Roslyn 5.9.0-1.26279.1 and later. Once the compiler bundled by - // this repo's SDK reaches that version this pragma and the - // `armsMergeDeclaredCases` plumbing can be removed. - bool needsExhaustivenessPragma = switchArmCount == 1 || armsMergeDeclaredCases; - if (needsExhaustivenessPragma) - { - writer.WriteLine("#pragma warning disable CS8509 // https://github.com/dotnet/roslyn/issues/83666"); - } - + // declared case set being exhaustively covered by its arms. writer.WriteLine($"UnionDeconstructor = static ({genericArg} value) =>"); writer.WriteLine('{'); writer.Indentation++; @@ -816,11 +813,6 @@ private static SourceText GenerateForUnion(ContextGenerationSpec contextSpec, Ty writer.WriteLine("};"); writer.Indentation--; writer.WriteLine("},"); - - if (needsExhaustivenessPragma) - { - writer.WriteLine("#pragma warning restore CS8509"); - } } writer.WriteLine("TypeClassifier = null,"); @@ -2119,6 +2111,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti if (optionsSpec.IncludeFields is bool includeFields) writer.WriteLine($"IncludeFields = {FormatBoolLiteral(includeFields)},"); + if (optionsSpec.InferClosedTypePolymorphism is bool inferClosedTypePolymorphism) + writer.WriteLine($"InferClosedTypePolymorphism = {FormatBoolLiteral(inferClosedTypePolymorphism)},"); + if (optionsSpec.MaxDepth is int maxDepth) writer.WriteLine($"MaxDepth = {maxDepth},"); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index fd06fa01fdd78b..b6580abd9ebbde 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -47,6 +47,19 @@ private sealed class Parser internal const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute"; + // Exception message baked into generated metadata for derived types that cannot be + // resolved against their base. Like other generated exception strings (and unlike + // source-gen diagnostics) it is intentionally not localized so generated output stays + // deterministic across build cultures. {0} = derived type, {1} = base type. + private const string OpenGenericDerivedTypeCouldNotBeResolvedExceptionMessage = + "The open generic derived type '{0}' could not be resolved for the polymorphic base type '{1}'."; + + // Exception message baked into generated metadata for inferred derived types that are + // less accessible than their base. Not localized for the same determinism reasons as + // above. {0} = derived type, {1} = base type. + private const string InferredDerivedTypeIsNotAccessibleExceptionMessage = + "The inferred derived type '{0}' must be at least as accessible as the polymorphic base type '{1}'."; + private readonly KnownTypeSymbols _knownSymbols; private readonly bool _compilationContainsCoreJsonTypes; @@ -433,6 +446,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN char? indentCharacter = null; int? indentSize = null; bool? allowDuplicateProperties = null; + bool? inferClosedTypePolymorphism = null; if (attributeData.ConstructorArguments.Length > 0) { @@ -562,6 +576,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN allowDuplicateProperties = (bool)namedArg.Value.Value!; break; + case nameof(JsonSourceGenerationOptionsAttribute.InferClosedTypePolymorphism): + inferClosedTypePolymorphism = (bool)namedArg.Value.Value!; + break; + case nameof(JsonSourceGenerationOptionsAttribute.TypeClassifiers): typeClassifiers = new List(); foreach (TypedConstant element in namedArg.Value.Values) @@ -615,6 +633,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN IndentCharacter = indentCharacter, IndentSize = indentSize, AllowDuplicateProperties = allowDuplicateProperties, + InferClosedTypePolymorphism = inferClosedTypePolymorphism, }; } @@ -697,7 +716,7 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener bool implementsIJsonOnSerialized = false; bool implementsIJsonOnSerializing = false; - ProcessTypeCustomAttributes(typeToGenerate, contextType, + ProcessTypeCustomAttributes(typeToGenerate, contextType, options, ref experimentalIds, out JsonNumberHandling? numberHandling, out JsonUnmappedMemberHandling? unmappedMemberHandling, @@ -943,6 +962,7 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener private void ProcessTypeCustomAttributes( in TypeToGenerate typeToGenerate, INamedTypeSymbol contextType, + SourceGenerationOptionsSpec? options, ref HashSet? experimentalIds, out JsonNumberHandling? numberHandling, out JsonUnmappedMemberHandling? unmappedMemberHandling, @@ -970,6 +990,8 @@ private void ProcessTypeCustomAttributes( string? typeDiscriminatorPropertyName = null; TypeRef? polymorphicClassifierFactoryType = null; List? derivedTypes = null; + string? unresolvedDerivedTypeError = null; + ImmutableArray closedTypeDerivedTypes = default; bool hasUnionTypeClassifierSpecified = false; bool isUnionType = IsUnionType(typeToGenerate.Type); INamedTypeSymbol? namedUnionType = typeToGenerate.Type as INamedTypeSymbol; @@ -1048,11 +1070,30 @@ private void ProcessTypeCustomAttributes( out INamedTypeSymbol? resolvedType, out string? failureReason)) { ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, attributeData.GetLocation(), derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason); + + // An open generic derived type that cannot be unified against this + // base has no concrete type to emit. Rather than dropping the entry + // (which would silently serialize the base non-polymorphically), record + // the failure so the emitter generates a JsonTypeInfo factory that throws + // the same way the reflection-based resolver does. + unresolvedDerivedTypeError ??= string.Format( + CultureInfo.InvariantCulture, + OpenGenericDerivedTypeCouldNotBeResolvedExceptionMessage, + derivedType.ToDisplayString(), + typeToGenerate.Type.ToDisplayString()); continue; } derivedType = resolvedType!; } + else if (!typeToGenerate.Type.IsAssignableFrom(derivedType)) + { + // A closed derived type that is not assignable to the base is not a + // supported polymorphic derived type. The shared runtime resolver already + // throws DerivedTypeNotSupported for the emitted entry; surface the problem + // at build time with a warning so it is not missed. + ReportDiagnostic(DiagnosticDescriptors.DerivedTypeIsNotSupported, attributeData.GetLocation(), derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); + } TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode); @@ -1118,9 +1159,31 @@ namedUnionType is not null && } } } + else if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.IsClosedTypeAttributeType)) + { + foreach (KeyValuePair namedArg in attributeData.NamedArguments) + { + if (namedArg.Key == "DerivedTypes") + { + closedTypeDerivedTypes = namedArg.Value.Values; + break; + } + } + } + } + + // Infer polymorphism from a closed type hierarchy when InferClosedTypePolymorphism is + // enabled, the base type is marked [IsClosedType] with a non-empty derived type set, and + // no explicit [JsonDerivedType] attributes were specified (explicit registration wins). + if (options?.InferClosedTypePolymorphism is true && + derivedTypes is null && + unresolvedDerivedTypeError is null && + !closedTypeDerivedTypes.IsDefaultOrEmpty) + { + InferClosedTypeDerivedTypes(typeToGenerate, closedTypeDerivedTypes, ref derivedTypes, ref unresolvedDerivedTypeError); } - if (hasPolymorphicAttribute || derivedTypes is { Count: > 0 }) + if (hasPolymorphicAttribute || derivedTypes is { Count: > 0 } || unresolvedDerivedTypeError is not null) { polymorphismOptions = new PolymorphismOptionsSpec { @@ -1129,6 +1192,7 @@ namedUnionType is not null && TypeClassifierFactoryType = polymorphicClassifierFactoryType, TypeDiscriminatorPropertyName = typeDiscriminatorPropertyName, UnknownDerivedTypeHandling = unknownDerivedTypeHandling, + UnresolvedDerivedTypeError = unresolvedDerivedTypeError, }; } @@ -1140,6 +1204,120 @@ namedUnionType is not null && } } + /// + /// Synthesizes entries from the [IsClosedType] derived + /// type set, mirroring the reflection-side inference in + /// DefaultJsonTypeInfoResolver.Helpers.PopulatePolymorphismMetadata. Each inferred + /// entry uses the derived type's metadata name as its string discriminator. + /// + private void InferClosedTypeDerivedTypes( + in TypeToGenerate typeToGenerate, + ImmutableArray closedTypeDerivedTypes, + ref List? derivedTypes, + ref string? unresolvedDerivedTypeError) + { + int baseAccessibility = GetEffectiveAccessibility(typeToGenerate.Type); + HashSet? seenDiscriminators = null; + + foreach (TypedConstant closedTypeConstant in closedTypeDerivedTypes) + { + if (closedTypeConstant.Value is not ITypeSymbol closedDerivedType) + { + continue; + } + + ITypeSymbol derivedType = closedDerivedType; + + if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived) + { + if (!TryResolveOpenGenericDerivedType(unboundDerived, typeToGenerate.Type, out INamedTypeSymbol? resolvedType, out _)) + { + // An inferred open generic derived type that cannot be unified against this + // base has no concrete type to emit. The reflection path throws here, so + // record the failure and let the emitter generate a throwing factory rather + // than silently serializing the base non-polymorphically. + unresolvedDerivedTypeError ??= string.Format( + CultureInfo.InvariantCulture, + OpenGenericDerivedTypeCouldNotBeResolvedExceptionMessage, + derivedType.ToDisplayString(), + typeToGenerate.Type.ToDisplayString()); + continue; + } + + derivedType = resolvedType!; + } + else if (!typeToGenerate.Type.IsAssignableFrom(derivedType)) + { + // Not assignable to the base: the shared runtime resolver throws + // DerivedTypeNotSupported for the emitted entry, matching reflection. Surface the + // problem at build time and keep the entry so the runtime behavior matches. + ReportDiagnostic(DiagnosticDescriptors.DerivedTypeIsNotSupported, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); + } + + if (GetEffectiveAccessibility(derivedType) < baseAccessibility) + { + // The reflection path throws for inferred derived types less accessible than the + // base; mirror that by emitting a throwing factory instead of dropping the entry. + ReportDiagnostic(DiagnosticDescriptors.InferredDerivedTypeIsNotAccessible, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); + unresolvedDerivedTypeError ??= string.Format( + CultureInfo.InvariantCulture, + InferredDerivedTypeIsNotAccessibleExceptionMessage, + derivedType.ToDisplayString(), + typeToGenerate.Type.ToDisplayString()); + continue; + } + + string discriminator = closedDerivedType.MetadataName; + if (!(seenDiscriminators ??= new(StringComparer.Ordinal)).Add(discriminator)) + { + // Emit the colliding entry anyway so the shared runtime resolver throws, + // matching the reflection path; surface the collision at build time. + ReportDiagnostic(DiagnosticDescriptors.InferredDerivedTypeDiscriminatorCollision, typeToGenerate.Location, discriminator, typeToGenerate.Type.ToDisplayString()); + } + + if (derivedTypes is null && typeToGenerate.Mode == JsonSourceGenerationMode.Serialization) + { + ReportDiagnostic(DiagnosticDescriptors.PolymorphismNotSupported, typeToGenerate.Location, typeToGenerate.Type.ToDisplayString()); + } + + TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode); + (derivedTypes ??= new()).Add(new DerivedTypeSpec + { + DerivedType = derivedTypeRef, + TypeDiscriminator = discriminator, + }); + } + } + + /// + /// Computes the effective accessibility rank of a type for closed-hierarchy inference: + /// 2 = publicly visible, 1 = internal/assembly visible, 0 = private or protected. + /// The effective rank is the most restrictive level across the type's nesting chain. + /// Mirrors DefaultJsonTypeInfoResolver.Helpers.GetEffectiveAccessibility. + /// + private static int GetEffectiveAccessibility(ITypeSymbol type) + { + int rank = 2; + + for (ITypeSymbol? current = type; current is not null; current = current.ContainingType) + { + int level = current.DeclaredAccessibility switch + { + Accessibility.Public => 2, + Accessibility.Internal or Accessibility.ProtectedOrInternal => 1, + Accessibility.Protected or Accessibility.ProtectedAndInternal or Accessibility.Private => 0, + _ => 2, + }; + + if (level < rank) + { + rank = level; + } + } + + return rank; + } + /// /// Source-gen-side resolver: closes against /// via structural unification at compile time. diff --git a/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs index 24c20a78ec1b35..dc3946eb142170 100644 --- a/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs @@ -26,5 +26,15 @@ public sealed record PolymorphismOptionsSpec public required string? TypeDiscriminatorPropertyName { get; init; } public required JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; init; } + + /// + /// When set, a registered derived type could not be resolved against this base type + /// (e.g. an open generic derived type that fails unification). The value is a + /// pre-formatted, non-localized message describing the failure. The emitter generates + /// a factory that + /// throws this message so the generated metadata fails the same way reflection does, + /// instead of silently serializing the base type non-polymorphically. + /// + public required string? UnresolvedDerivedTypeError { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs index 746c202355a507..fafcec03e88a59 100644 --- a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs @@ -76,6 +76,8 @@ public sealed record SourceGenerationOptionsSpec public required bool? AllowDuplicateProperties { get; init; } + public required bool? InferClosedTypePolymorphism { get; init; } + public JsonKnownNamingPolicy? GetEffectivePropertyNamingPolicy() => PropertyNamingPolicy ?? (Defaults is JsonSerializerDefaults.Web ? JsonKnownNamingPolicy.CamelCase : null); } diff --git a/src/libraries/System.Text.Json/gen/Resources/Strings.resx b/src/libraries/System.Text.Json/gen/Resources/Strings.resx index 446fe72efc6ca2..4f38b1aaea827b 100644 --- a/src/libraries/System.Text.Json/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/gen/Resources/Strings.resx @@ -219,6 +219,24 @@ The open generic derived type '{0}' could not be resolved for the polymorphic base type '{1}': {2}. + + Derived type is not a supported polymorphic derived type. + + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + Inferred derived type is less accessible than the polymorphic base type. + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + Inferred derived types produce a duplicate type discriminator. + + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + the derived type is not assignable to the base type diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf index b55f6d6983d368..a43d040a35227e 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf @@ -32,6 +32,16 @@ Zdrojový generátor nepodporuje atributy odvozené od atributu JsonConverterAttribute. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Existuje více typů s názvem „{0}“. Zdroj byl vygenerován pro první zjištěný zdroj. K vyřešení této kolize použijte JsonSerializableAttribute.TypeInfoPropertyName. @@ -52,6 +62,26 @@ Atribut JsonDerivedTypeAttribute se v JsonSourceGenerationMode.Serialization nepodporuje. + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. Typ JsonConverterAttribute {0} specifikovaný u členu {1} není typem konvertoru nebo neobsahuje přístupný konstruktor bez parametrů. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf index c0d0eaef022cfb..d666f15f9c3d7e 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf @@ -32,6 +32,16 @@ Von JsonConverterAttribute abgeleitete Attribute werden vom Quellgenerator nicht unterstützt. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Es gibt mehrere Typen mit dem Namen "{0}". Die Quelle wurde für die erste erkannte Quelle generiert. Verwenden Sie "JsonSerializableAttribute.TypeInfoPropertyName", um diesen Konflikt zu beheben. @@ -52,6 +62,26 @@ „JsonDerivedTypeAttribute“ wird in „JsonSourceGenerationMode.Serialization“ nicht unterstützt. + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. Der für den Member "{1}" angegebene JsonConverterAttribute-Typ "{0}" ist kein Konvertertyp oder enthält keinen parameterlosen Konstruktor, auf den zugegriffen werden kann. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf index c80268035c6e94..bf1c82960b1bd8 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf @@ -32,6 +32,16 @@ El generador de origen no admite los atributos derivados de JsonConverterAttribute. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Hay varios tipos denominados "{0}". El origen se generó para el primero detectado. Use "JsonSerializableAttribute.TypeInfoPropertyName" para resolver esta colisión. @@ -52,6 +62,26 @@ \"JsonDerivedTypeAttribute\" no se admite en \"JsonSourceGenerationMode.Serialization\". + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. El tipo “JsonConverterAttribute” “{0}” especificado en el miembro “{1}” no es un tipo de convertidor o no contiene un constructor sin parámetros accesible. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf index 3f3a6e8c87beb5..64f8d4074ef17a 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf @@ -32,6 +32,16 @@ Les attributs dérivant de JsonConverterAttribute ne sont pas pris en charge par le générateur de source. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Il existe plusieurs types nommés '{0}'. La source a été générée pour le premier détecté. Utilisez 'JsonSerializableAttribute.TypeInfoPropertyName' pour résoudre cette collision. @@ -52,6 +62,26 @@ « JsonDerivedTypeAttribute » n’est pas pris en charge dans « JsonSourceGenerationMode.Serialization ». + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. Le type 'JsonConverterAttribute' '{0}' spécifié sur le membre '{1}' n’est pas un type convertisseur ou ne contient pas de constructeur sans paramètre accessible. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf index 21a23d538faefa..8bbbab9ae8597f 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf @@ -32,6 +32,16 @@ Gli attributi che derivano da JsonConverterAttribute non sono supportati dal generatore di origine. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Sono presenti più tipi denominati '{0}'. L'origine è stata generata per il primo tipo rilevato. Per risolvere questa collisione, usare 'JsonSerializableAttribute.TypeInfoPropertyName'. @@ -52,6 +62,26 @@ 'JsonDerivedTypeAttribute' non è supportato in 'JsonSourceGenerationMode.Serialization'. + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. Il tipo 'JsonConverterAttribute' '{0}' specificato nel membro '{1}' non è un tipo di convertitore o non contiene un costruttore senza parametri accessibile. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf index db1cbf88bda348..4112589fb4729e 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf @@ -32,6 +32,16 @@ JsonConverterAttribute から派生する属性は、ソース ジェネレーターではサポートされていません。 + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. '{0}' と名前が付けられた種類が複数あります。最初に検出されたものに対してソースが生成されました。この問題を解決するには、'JsonSerializableAttribute.TypeInfoPropertyName' を使用します。 @@ -52,6 +62,26 @@ 'JsonDerivedTypeAttribute' は 'JsonSourceGenerationMode.Serialization' ではサポートされていません。 + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. メンバー '{1}' で指定されている 'JsonConverterAttribute' 型 '{0}' はコンバーター型ではないか、アクセス可能なパラメーターなしのコンストラクターを含んでいません。 diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf index 40625805982421..f0373fbabbcfad 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf @@ -32,6 +32,16 @@ JsonConverterAttribute에서 파생되는 특성은 소스 생성기에서 지원되지 않습니다. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. 이름이 '{0}'인 형식이 여러 개 있습니다. 처음 검색된 것에 대한 원본이 생성되었습니다. 이 충돌을 해결하려면 'JsonSerializableAttribute.TypeInfoPropertyName'을 사용하세요. @@ -52,6 +62,26 @@ 'JsonSourceGenerationMode.Serialization'에서는 'JsonDerivedTypeAttribute'가 지원되지 않습니다. + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. '{1}' 멤버에 지정된 'JsonConverterAttribute' 형식 '{0}'이(가) 변환기 형식이 아니거나 액세스 가능한 매개 변수가 없는 생성자를 포함하지 않습니다. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf index 17dffc837a5103..3f61e2966733cf 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf @@ -32,6 +32,16 @@ Atrybuty pochodzące z atrybutu JsonConverterAttribute nie są obsługiwane przez generator źródła. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Istnieje wiele typów o nazwie „{0}”. Wygenerowano źródło dla pierwszego wykrytego elementu. Aby rozwiązać tę kolizję, użyj elementu „JsonSerializableAttribute. TypeInfoPropertyName”. @@ -52,6 +62,26 @@ Atrybut „JsonDerivedTypeAttribute” nie jest obsługiwany w elemecie „JsonSourceGenerationMode.Serialization”. + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. Typ „{0}” „JsonConverterAttribute” określony w przypadku składowej „{1}” nie jest typem konwertera lub nie zawiera dostępnego konstruktora bez parametrów. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf index f9438ee48ed62a..dbf22e3a2de60d 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf @@ -32,6 +32,16 @@ Os atributos derivados de JsonConverterAttribute não têm suporte pelo gerador de origem. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Existem vários tipos chamados '{0}'. A fonte foi gerada para o primeiro detectado. Use 'JsonSerializableAttribute.TypeInfoPropertyName' para resolver essa colisão. @@ -52,6 +62,26 @@ 'JsonDerivedTypeAttribute' não tem suporte em 'JsonSourceGenerationMode.Serialization'. + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. O tipo "JsonConverterAttribute" "{0}" especificado no membro "{1}" não é um tipo de conversor ou não contém um construtor sem parâmetros acessível. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf index 46daaa8228822e..80e734c1cae370 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf @@ -32,6 +32,16 @@ Атрибуты, производные от JsonConverterAttribute, не поддерживаются генератором исходного кода. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. Существует несколько типов с именем "{0}". Исходный код сформирован для первого обнаруженного типа. Используйте JsonSerializableAttribute.TypeInfoPropertyName для устранения этого конфликта. @@ -52,6 +62,26 @@ Атрибут JsonDerivedTypeAttribute не поддерживается в \"JsonSourceGenerationMode.Serialization\". + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. Тип "JsonConverterAttribute" "{0}", указанный в элементе "{1}", не является типом преобразователя или не содержит доступного конструктора без параметров. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf index 961644457743a8..fc8d873e840c59 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf @@ -32,6 +32,16 @@ JsonConverterAttribute’tan türetilen öznitelikler kaynak oluşturucu tarafından desteklenmiyor. + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. '{0}' adını taşıyan birden çok tür var. Kaynak, algılanan ilk tür için oluşturuldu. Bu çarpışmayı çözmek için 'JsonSerializableAttribute.TypeInfoPropertyName' özelliğini kullanın. @@ -52,6 +62,26 @@ 'JsonSourceGenerationMode.Serialization' içinde 'JsonDerivedTypeAttribute' desteklenmiyor. + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. '{1}' üyesi üzerinde belirtilen 'JsonConverterAttribute' '{0}' türü dönüştürücü türü değil veya erişilebilir parametresiz bir oluşturucu içermiyor. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf index bba5d574bb8539..0f5dd70f1453c6 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -32,6 +32,16 @@ 源生成器不支持从 JsonConverterAttribute 派生的属性。 + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. 有多个名为 {0} 的类型。已为第一个检测到类型的生成源。请使用“JsonSerializableAttribute.TypeInfoPropertyName”以解决此冲突。 @@ -52,6 +62,26 @@ \"JsonSourceGenerationMode.Serialization\" 中不支持 \"JsonDerivedTypeAttribute\"。 + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. 在成员 "{1}" 上指定的 "JsonConverterAttribute" 类型 "{0}" 不是转换器类型或不包含可访问的无参数构造函数。 diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf index 2f64e9918c993d..beb737cfc03c84 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -32,6 +32,16 @@ 來源產生器不支援衍生自 JsonConverterAttribute 的屬性。 + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type. + + + + Derived type is not a supported polymorphic derived type. + Derived type is not a supported polymorphic derived type. + + There are multiple types named '{0}'. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision. 有多個名為 '{0}' 的類型。已為偵測到的第一個項目產生來源。請使用 'JsonSerializableAttribute.TypeInfoPropertyName' 解決此衝突。 @@ -52,6 +62,26 @@ 'JsonSourceGenerationMode.Serialization' 中不支援 'JsonDerivedTypeAttribute'。 + + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + + + Inferred derived types produce a duplicate type discriminator. + Inferred derived types produce a duplicate type discriminator. + + + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + + + + Inferred derived type is less accessible than the polymorphic base type. + Inferred derived type is less accessible than the polymorphic base type. + + The 'JsonConverterAttribute' type '{0}' specified on member '{1}' is not a converter type or does not contain an accessible parameterless constructor. 成員 '{1}' 上指定的 'JsonConverterAttribute' 類型 '{0}' 不是轉換器類型,或不包含可存取的無參數建構函式。 diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index f0280f4e5c68c2..620cff0fb6a5ec 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -440,6 +440,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public bool IgnoreReadOnlyFields { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool IncludeFields { get { throw null; } set { } } + public bool InferClosedTypePolymorphism { get { throw null; } set { } } public bool IsReadOnly { get { throw null; } } public int MaxDepth { get { throw null; } set { } } public string NewLine { get { throw null; } set { } } @@ -1202,6 +1203,7 @@ public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefau public bool IgnoreReadOnlyFields { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool IncludeFields { get { throw null; } set { } } + public bool InferClosedTypePolymorphism { get { throw null; } set { } } public int MaxDepth { get { throw null; } set { } } public string? NewLine { get { throw null; } set { } } public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index d0bfc8e0e21d72..d0f454e002f271 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -689,6 +689,9 @@ The polymorphic type '{0}' has already specified a type discriminator '{1}'. + + The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. + The metadata property names '$id', '$ref', and '$values' are reserved and cannot be used as custom type discriminator property names. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index 334d8bf8f43477..ea8e1b5330aadf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -543,6 +543,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) left._indentSize == right._indentSize && left._typeInfoResolver == right._typeInfoResolver && left._allowDuplicateProperties == right._allowDuplicateProperties && + left._inferClosedTypePolymorphism == right._inferClosedTypePolymorphism && CompareLists(left._converters, right._converters) && CompareLists(left._typeClassifiers, right._typeClassifiers); @@ -605,6 +606,7 @@ public int GetHashCode(JsonSerializerOptions options) AddHashCode(ref hc, options._indentSize); AddHashCode(ref hc, options._typeInfoResolver); AddHashCode(ref hc, options._allowDuplicateProperties); + AddHashCode(ref hc, options._inferClosedTypePolymorphism); AddListHashCode(ref hc, options._converters); AddListHashCode(ref hc, options._typeClassifiers); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 2365589defdc1f..530a3e858e529a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -104,6 +104,7 @@ public static JsonSerializerOptions Strict private char _indentCharacter = JsonConstants.DefaultIndentCharacter; private int _indentSize = JsonConstants.DefaultIndentSize; private bool _allowDuplicateProperties = true; + private bool _inferClosedTypePolymorphism; /// /// Constructs a new instance. @@ -158,6 +159,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _indentCharacter = options._indentCharacter; _indentSize = options._indentSize; _allowDuplicateProperties = options._allowDuplicateProperties; + _inferClosedTypePolymorphism = options._inferClosedTypePolymorphism; _typeInfoResolver = options._typeInfoResolver; EffectiveMaxDepth = options.EffectiveMaxDepth; ReferenceHandlingStrategy = options.ReferenceHandlingStrategy; @@ -874,6 +876,29 @@ public bool AllowDuplicateProperties } } + /// + /// Gets or sets a value indicating whether polymorphic serialization metadata is inferred for + /// types that the compiler has marked as closed type hierarchies. + /// + /// + /// Thrown if this property is set after serialization or deserialization has occurred. + /// + /// + /// By default, it's set to . When set to , types that + /// declare a closed set of derived types (and that do not specify an explicit + /// list) are treated as polymorphic, with one + /// derived type registered per member of the closed hierarchy. + /// + public bool InferClosedTypePolymorphism + { + get => _inferClosedTypePolymorphism; + set + { + VerifyMutable(); + _inferClosedTypePolymorphism = value; + } + } + /// /// Returns true if options uses compatible built-in resolvers or a combination of compatible built-in resolvers. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index ef694d8351dda7..d59065f878b784 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -87,6 +87,29 @@ internal static void PopulatePolymorphismMetadata(JsonTypeInfo typeInfo) JsonPolymorphismOptions? options = JsonPolymorphismOptions.CreateFromAttributeDeclarations(typeInfo.Type, out JsonPolymorphicAttribute? polymorphicAttribute); +#if NET11_0_OR_GREATER + // IsClosedTypeAttribute is a .NET 11 addition emitted by the compiler for closed + // hierarchies, so closed-type polymorphism inference is only possible on runtimes + // where the attribute type exists. On down-level targets the feature is a no-op. + if (typeInfo.Options.InferClosedTypePolymorphism && + (options is null || options.DerivedTypes.Count == 0) && + typeInfo.Type.GetCustomAttribute(inherit: false) is { DerivedTypes.Length: > 0 } closedTypeAttribute) + { + options ??= new(); + int baseAccessibility = GetEffectiveAccessibility(typeInfo.Type); + + foreach (Type derivedType in closedTypeAttribute.DerivedTypes) + { + if (GetEffectiveAccessibility(derivedType) < baseAccessibility) + { + ThrowHelper.ThrowInvalidOperationException_InferredDerivedTypeIsNotAccessible(typeInfo.Type, derivedType); + } + + options.DerivedTypes.Add(new JsonDerivedType(derivedType, derivedType.Name)); + } + } +#endif + if (options is not null) { ResolveOpenGenericDerivedTypes(typeInfo.Type, options.DerivedTypes); @@ -156,6 +179,42 @@ private static void ResolveOpenGenericDerivedTypes(Type baseType, IList + /// Computes the effective accessibility rank of a type for closed-hierarchy inference: + /// 2 = publicly visible, 1 = internal/assembly visible, 0 = private or protected. + /// The effective rank is the most restrictive level across the type's nesting chain. + /// + private static int GetEffectiveAccessibility(Type type) + { + int rank = 2; + + for (Type? current = type; current is not null; current = current.IsNested ? current.DeclaringType : null) + { + int level; + if (current.IsPublic || current.IsNestedPublic) + { + level = 2; + } + else if (current.IsNestedFamily || current.IsNestedFamANDAssem || current.IsNestedPrivate) + { + level = 0; + } + else + { + level = 1; + } + + if (level < rank) + { + rank = level; + } + } + + return rank; + } +#endif + /// /// Reflection-side resolver: closes against the /// constructed base type identified by (, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index ebfc4e44761b92..7066f052edc21f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -967,6 +967,12 @@ public static void ThrowInvalidOperationException_TypeDicriminatorIdIsAlreadySpe throw new InvalidOperationException(SR.Format(SR.Polymorphism_TypeDicriminatorIdIsAlreadySpecified, baseType, typeDiscriminator)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_InferredDerivedTypeIsNotAccessible(Type baseType, Type derivedType) + { + throw new InvalidOperationException(SR.Format(SR.Polymorphism_InferredDerivedTypeIsNotAccessible, derivedType, baseType)); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_InvalidCustomTypeDiscriminatorPropertyName() { diff --git a/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs b/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs index c74c9b1902ca39..fbe965da0e92cc 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs @@ -550,6 +550,7 @@ await Assert.ThrowsAsync( async () => await Serializer.DeserializeWrapper(json, options)); } +#pragma warning disable SYSLIB1240 // Derived types are intentionally not assignable to the base type for these tests. [JsonDerivedType(typeof(BaseClassWithPolymorphism), "base")] [JsonDerivedType(typeof(DerivedClass_DerivingFrom_BaseClassWithPolymorphism), "derived")] public class BaseClassRecursive @@ -559,6 +560,7 @@ public class BaseClassRecursive public string BaseClassProp { get; set; } = "BaseInitial"; } +#pragma warning restore SYSLIB1240 public class DerivedClass_DerivingFrom_BaseClassRecursive : BaseClassRecursive { diff --git a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs new file mode 100644 index 00000000000000..4f63519ab6c294 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET11_0_OR_GREATER +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public abstract partial class PolymorphicTests + { + // The closed-type hierarchies exercised below live in the IL fixture assembly + // System.Text.Json.ClosedTypeTestFixtures. They are annotated with + // [IsClosedType], which the C# compiler reserves for its own use, so the metadata + // cannot be authored in C#. These tests always run against the reflection-based + // resolver (even under source-gen subclasses) by supplying an explicit + // DefaultJsonTypeInfoResolver, so they validate the reflection inference path. + private static JsonSerializerOptions CreateClosedTypeInferenceOptions(bool infer = true) => + new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + InferClosedTypePolymorphism = infer, + }; + + [Fact] + public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscriminator() + { + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + ClosedShape value = new ClosedCircle(); + string json = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual("""{"$type":"ClosedCircle"}""", json); + + ClosedShape roundtripped = await Serializer.DeserializeWrapper(json, options); + Assert.IsType(roundtripped); + } + + [Fact] + public async Task ClosedTypeInference_FlagDisabled_DoesNotInferPolymorphism() + { + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(infer: false); + + ClosedShape value = new ClosedCircle(); + string json = await Serializer.SerializeWrapper(value, options); + + Assert.DoesNotContain("$type", json); + } + + [Fact] + public async Task ClosedTypeInference_EmptyDerivedTypes_IsInert() + { + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + EmptyClosedBase value = new EmptyClosedDerived(); + string json = await Serializer.SerializeWrapper(value, options); + + Assert.DoesNotContain("$type", json); + } + + [Fact] + public async Task ClosedTypeInference_OpenGenericDerived_IsInferredAndResolved() + { + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + ClosedContainer value = new ClosedBox(); + string json = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual("""{"$type":"ClosedBox`1"}""", json); + + ClosedContainer roundtripped = await Serializer.DeserializeWrapper>(json, options); + Assert.IsType>(roundtripped); + } + + [Fact] + public async Task ClosedTypeInference_DuplicateDiscriminator_ThrowsInvalidOperationException() + { + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + ClosedCollisionBase value = new ClosedCollisionHolderA.Node(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, options)); + } + + [Fact] + public async Task ClosedTypeInference_InaccessibleDerivedType_ThrowsInvalidOperationException() + { + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + // The closed hierarchy mixes a public and an internal derived type. The internal + // type is less accessible than the public base, so inference must reject it. + PublicClosedBase value = new PublicClosedDerived(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, options)); + } + } +} +#endif diff --git a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs index c3d47893e33feb..cdc813768e29c2 100644 --- a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs +++ b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs @@ -2497,10 +2497,12 @@ public async Task PolymorphicClassWithStructDerivedTypeAttribute_ThrowsInvalidOp await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1240 // Derived type is intentionally not assignable to the base type for these tests. [JsonDerivedType(typeof(Guid))] public class PolymorphicClassWithStructDerivedTypeAttribute { } +#pragma warning restore SYSLIB1240 [Fact] public async Task PolymorphicClassWithObjectDerivedTypeAttribute_ThrowsInvalidOperationException() @@ -2509,10 +2511,12 @@ public async Task PolymorphicClassWithObjectDerivedTypeAttribute_ThrowsInvalidOp await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1240 // Derived type is intentionally not assignable to the base type for these tests. [JsonDerivedType(typeof(object), "object")] public class PolymorphicClassWithObjectDerivedTypeAttribute { } +#pragma warning restore SYSLIB1240 [Fact] public async Task PolymorphicClassWithNonAssignableDerivedTypeAttribute_ThrowsInvalidOperationException() @@ -2521,10 +2525,12 @@ public async Task PolymorphicClassWithNonAssignableDerivedTypeAttribute_ThrowsIn await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1240 // Derived type is intentionally not assignable to the base type for these tests. [JsonDerivedType(typeof(object))] public class PolymorphicClassWithNonAssignableDerivedTypeAttribute { } +#pragma warning restore SYSLIB1240 [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il new file mode 100644 index 00000000000000..c776effb3af77d --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This assembly provides closed-type hierarchies annotated with +// System.Runtime.CompilerServices.IsClosedTypeAttribute. That attribute is reserved for +// compiler usage (applying it in C# source is a hard CS8335 error), and the 'closed' +// keyword that would make the C# compiler emit it is not yet available, so the metadata +// is hand-authored in IL here. It is consumed by closed-type polymorphism inference tests +// for both the reflection-based and source-generated serializers. +// +// The IsClosedTypeAttribute custom-attribute blob uses a single named property +// 'DerivedTypes' of type System.Type[]. Types are intentionally property-less: the tests +// assert on the emitted '$type' discriminator and the deserialized runtime type, which is +// sufficient to prove that polymorphism was inferred. + +.assembly extern System.Runtime +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A) +} + +.assembly System.Text.Json.ClosedTypeTestFixtures +{ + .ver 1:0:0:0 +} + +.module System.Text.Json.ClosedTypeTestFixtures.dll + +.namespace System.Text.Json.Serialization.Tests +{ + // ---- Basic closed hierarchy ---------------------------------------------------------- + .class public abstract auto ansi beforefieldinit ClosedShape + extends [System.Runtime]System.Object + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { + property type[] 'DerivedTypes' = type[2](System.Text.Json.Serialization.Tests.ClosedCircle System.Text.Json.Serialization.Tests.ClosedSquare) + } + .method family hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } + } + + .class public auto ansi sealed beforefieldinit ClosedCircle + extends System.Text.Json.Serialization.Tests.ClosedShape + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void System.Text.Json.Serialization.Tests.ClosedShape::.ctor() + ret + } + } + + .class public auto ansi sealed beforefieldinit ClosedSquare + extends System.Text.Json.Serialization.Tests.ClosedShape + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void System.Text.Json.Serialization.Tests.ClosedShape::.ctor() + ret + } + } + + // ---- Empty closed hierarchy (inference must stay inert) ------------------------------ + .class public abstract auto ansi beforefieldinit EmptyClosedBase + extends [System.Runtime]System.Object + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = ( + 01 00 00 00 + ) + .method family hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } + } + + .class public auto ansi sealed beforefieldinit EmptyClosedDerived + extends System.Text.Json.Serialization.Tests.EmptyClosedBase + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void System.Text.Json.Serialization.Tests.EmptyClosedBase::.ctor() + ret + } + } + + // ---- Open generic closed hierarchy --------------------------------------------------- + .class public abstract auto ansi beforefieldinit ClosedContainer`1 + extends [System.Runtime]System.Object + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { + property type[] 'DerivedTypes' = type[1](System.Text.Json.Serialization.Tests.ClosedBox`1) + } + .method family hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } + } + + .class public auto ansi sealed beforefieldinit ClosedBox`1 + extends class System.Text.Json.Serialization.Tests.ClosedContainer`1 + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void class System.Text.Json.Serialization.Tests.ClosedContainer`1::.ctor() + ret + } + } + + // ---- Discriminator collision (two derived types share the simple name 'Node') -------- + .class public abstract auto ansi beforefieldinit ClosedCollisionBase + extends [System.Runtime]System.Object + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { + property type[] 'DerivedTypes' = type[2](System.Text.Json.Serialization.Tests.ClosedCollisionHolderA/Node System.Text.Json.Serialization.Tests.ClosedCollisionHolderB/Node) + } + .method family hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } + } + + .class public abstract auto ansi sealed beforefieldinit ClosedCollisionHolderA + extends [System.Runtime]System.Object + { + .class nested public auto ansi sealed beforefieldinit Node + extends System.Text.Json.Serialization.Tests.ClosedCollisionBase + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void System.Text.Json.Serialization.Tests.ClosedCollisionBase::.ctor() + ret + } + } + } + + .class public abstract auto ansi sealed beforefieldinit ClosedCollisionHolderB + extends [System.Runtime]System.Object + { + .class nested public auto ansi sealed beforefieldinit Node + extends System.Text.Json.Serialization.Tests.ClosedCollisionBase + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void System.Text.Json.Serialization.Tests.ClosedCollisionBase::.ctor() + ret + } + } + } + + // ---- Accessibility rejection (public base, internal derived) ------------------------- + .class public abstract auto ansi beforefieldinit PublicClosedBase + extends [System.Runtime]System.Object + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { + property type[] 'DerivedTypes' = type[2](System.Text.Json.Serialization.Tests.PublicClosedDerived System.Text.Json.Serialization.Tests.InternalClosedDerived) + } + .method family hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } + } + + .class public auto ansi sealed beforefieldinit PublicClosedDerived + extends System.Text.Json.Serialization.Tests.PublicClosedBase + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void System.Text.Json.Serialization.Tests.PublicClosedBase::.ctor() + ret + } + } + + .class private auto ansi sealed beforefieldinit InternalClosedDerived + extends System.Text.Json.Serialization.Tests.PublicClosedBase + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void System.Text.Json.Serialization.Tests.PublicClosedBase::.ctor() + ret + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj new file mode 100644 index 00000000000000..c0b067c31f183b --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj @@ -0,0 +1,13 @@ + + + System.Text.Json.ClosedTypeTestFixtures + 1.0.0.0 + + $(NetCoreAppCurrent) + disable + Microsoft + + + + + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs index d8c6b1c4ecbb4a..e6d16145603d0b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs @@ -384,6 +384,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.ReferenceHandler)), ReferenceHandler.Preserve); yield return (GetProp(nameof(JsonSerializerOptions.TypeInfoResolver)), new DefaultJsonTypeInfoResolver()); yield return (GetProp(nameof(JsonSerializerOptions.AllowDuplicateProperties)), false /* true is default */); + yield return (GetProp(nameof(JsonSerializerOptions.InferClosedTypePolymorphism)), true); static PropertyInfo GetProp(string name) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index e152d9e04a93be..ba1ad8d6829210 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -100,6 +100,7 @@ + @@ -274,6 +275,11 @@ + + + + + From 1c9d973034dbf73cf249b52f6b17ffb1ced60708 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 29 Jun 2026 19:34:02 +0300 Subject: [PATCH 2/9] Replace baked runtime throw with build-time diagnostics for unresolved derived types Source-gen surfaces invalid polymorphic configurations through build-time SYSLIB diagnostics (the established design), so the prior approach of baking a non-localized `throw new InvalidOperationException(...)` into the generated metadata was inconsistent and not localizable. Remove the UnresolvedDerivedTypeError mechanism (the dispatch hook, the GenerateForTypeWithUnresolvedPolymorphism emitter method, the PolymorphismOptionsSpec field, and the two non-localized message constants) and restore the build-diagnostic + drop behavior: - Explicit open-generic [JsonDerivedType] that cannot be unified: report SYSLIB1229 and drop the entry (matches the pre-existing base behavior). - Inferred closed-type derived types that are open-generic-unresolvable or less accessible than the base: report SYSLIB1229 / SYSLIB1241 and drop the entry. Track explicit [JsonDerivedType] presence with a dedicated flag so closed-type inference is still suppressed when an explicit registration exists, even if every explicit entry failed to resolve. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../gen/JsonSourceGenerator.Emitter.cs | 26 -------- .../gen/JsonSourceGenerator.Parser.cs | 66 +++++-------------- .../gen/Model/PolymorphismOptionsSpec.cs | 10 --- 3 files changed, 18 insertions(+), 84 deletions(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 4c50be8edcca88..054858b398ce18 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -242,11 +242,6 @@ private static SourceText CompleteSourceFileAndReturnText(SourceWriter writer) private SourceText? GenerateTypeInfo(ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) { - if (typeGenerationSpec.PolymorphismOptions?.UnresolvedDerivedTypeError is not null) - { - return GenerateForTypeWithUnresolvedPolymorphism(contextSpec, typeGenerationSpec); - } - switch (typeGenerationSpec.ClassType) { case ClassType.BuiltInSupportType: @@ -358,27 +353,6 @@ private static SourceText GenerateForUnsupportedType(ContextGenerationSpec conte return CompleteSourceFileAndReturnText(writer); } - private static SourceText GenerateForTypeWithUnresolvedPolymorphism(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) - { - Debug.Assert(typeMetadata.PolymorphismOptions?.UnresolvedDerivedTypeError is not null); - - SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); - - GenerateTypeInfoFactoryHeader(writer, typeMetadata); - - // A registered derived type could not be resolved against this base type. Emit a - // factory body that throws the same way the reflection-based resolver does, rather - // than silently producing non-polymorphic metadata. The throw is the only statement - // inside the 'if (!TryGetTypeInfoForRuntimeCustomConverter(...))' block, so the - // normal 'return jsonTypeInfo' path (taken when a runtime custom converter applies) - // remains reachable and the method still compiles. - writer.WriteLine($"""throw new {InvalidOperationExceptionTypeRef}("{typeMetadata.PolymorphismOptions!.UnresolvedDerivedTypeError}");"""); - - GenerateTypeInfoFactoryFooter(writer); - - return CompleteSourceFileAndReturnText(writer); - } - private static SourceText GenerateForEnum(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec, experimentalDiagnosticIds: typeMetadata.ExperimentalDiagnosticIds); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index b6580abd9ebbde..8309edff903bfc 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -47,19 +47,6 @@ private sealed class Parser internal const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute"; - // Exception message baked into generated metadata for derived types that cannot be - // resolved against their base. Like other generated exception strings (and unlike - // source-gen diagnostics) it is intentionally not localized so generated output stays - // deterministic across build cultures. {0} = derived type, {1} = base type. - private const string OpenGenericDerivedTypeCouldNotBeResolvedExceptionMessage = - "The open generic derived type '{0}' could not be resolved for the polymorphic base type '{1}'."; - - // Exception message baked into generated metadata for inferred derived types that are - // less accessible than their base. Not localized for the same determinism reasons as - // above. {0} = derived type, {1} = base type. - private const string InferredDerivedTypeIsNotAccessibleExceptionMessage = - "The inferred derived type '{0}' must be at least as accessible as the polymorphic base type '{1}'."; - private readonly KnownTypeSymbols _knownSymbols; private readonly bool _compilationContainsCoreJsonTypes; @@ -990,7 +977,7 @@ private void ProcessTypeCustomAttributes( string? typeDiscriminatorPropertyName = null; TypeRef? polymorphicClassifierFactoryType = null; List? derivedTypes = null; - string? unresolvedDerivedTypeError = null; + bool hasExplicitDerivedTypeAttribute = false; ImmutableArray closedTypeDerivedTypes = default; bool hasUnionTypeClassifierSpecified = false; bool isUnionType = IsUnionType(typeToGenerate.Type); @@ -1061,6 +1048,7 @@ private void ProcessTypeCustomAttributes( if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonDerivedTypeAttributeType)) { Debug.Assert(attributeData.ConstructorArguments.Length > 0); + hasExplicitDerivedTypeAttribute = true; var derivedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!; if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived) @@ -1069,18 +1057,11 @@ private void ProcessTypeCustomAttributes( unboundDerived, typeToGenerate.Type, out INamedTypeSymbol? resolvedType, out string? failureReason)) { + // An open generic derived type that cannot be unified against this base + // has no concrete type to emit. Surface it at build time and drop the + // entry; source-gen reports invalid polymorphic configurations through + // diagnostics rather than baking runtime throws into generated metadata. ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, attributeData.GetLocation(), derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason); - - // An open generic derived type that cannot be unified against this - // base has no concrete type to emit. Rather than dropping the entry - // (which would silently serialize the base non-polymorphically), record - // the failure so the emitter generates a JsonTypeInfo factory that throws - // the same way the reflection-based resolver does. - unresolvedDerivedTypeError ??= string.Format( - CultureInfo.InvariantCulture, - OpenGenericDerivedTypeCouldNotBeResolvedExceptionMessage, - derivedType.ToDisplayString(), - typeToGenerate.Type.ToDisplayString()); continue; } @@ -1176,14 +1157,13 @@ namedUnionType is not null && // enabled, the base type is marked [IsClosedType] with a non-empty derived type set, and // no explicit [JsonDerivedType] attributes were specified (explicit registration wins). if (options?.InferClosedTypePolymorphism is true && - derivedTypes is null && - unresolvedDerivedTypeError is null && + !hasExplicitDerivedTypeAttribute && !closedTypeDerivedTypes.IsDefaultOrEmpty) { - InferClosedTypeDerivedTypes(typeToGenerate, closedTypeDerivedTypes, ref derivedTypes, ref unresolvedDerivedTypeError); + InferClosedTypeDerivedTypes(typeToGenerate, closedTypeDerivedTypes, ref derivedTypes); } - if (hasPolymorphicAttribute || derivedTypes is { Count: > 0 } || unresolvedDerivedTypeError is not null) + if (hasPolymorphicAttribute || derivedTypes is { Count: > 0 }) { polymorphismOptions = new PolymorphismOptionsSpec { @@ -1192,7 +1172,6 @@ unresolvedDerivedTypeError is null && TypeClassifierFactoryType = polymorphicClassifierFactoryType, TypeDiscriminatorPropertyName = typeDiscriminatorPropertyName, UnknownDerivedTypeHandling = unknownDerivedTypeHandling, - UnresolvedDerivedTypeError = unresolvedDerivedTypeError, }; } @@ -1213,8 +1192,7 @@ unresolvedDerivedTypeError is null && private void InferClosedTypeDerivedTypes( in TypeToGenerate typeToGenerate, ImmutableArray closedTypeDerivedTypes, - ref List? derivedTypes, - ref string? unresolvedDerivedTypeError) + ref List? derivedTypes) { int baseAccessibility = GetEffectiveAccessibility(typeToGenerate.Type); HashSet? seenDiscriminators = null; @@ -1230,17 +1208,13 @@ private void InferClosedTypeDerivedTypes( if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived) { - if (!TryResolveOpenGenericDerivedType(unboundDerived, typeToGenerate.Type, out INamedTypeSymbol? resolvedType, out _)) + if (!TryResolveOpenGenericDerivedType(unboundDerived, typeToGenerate.Type, out INamedTypeSymbol? resolvedType, out string? failureReason)) { // An inferred open generic derived type that cannot be unified against this - // base has no concrete type to emit. The reflection path throws here, so - // record the failure and let the emitter generate a throwing factory rather - // than silently serializing the base non-polymorphically. - unresolvedDerivedTypeError ??= string.Format( - CultureInfo.InvariantCulture, - OpenGenericDerivedTypeCouldNotBeResolvedExceptionMessage, - derivedType.ToDisplayString(), - typeToGenerate.Type.ToDisplayString()); + // base has no concrete type to emit. Surface it at build time and drop the + // entry, mirroring the established source-gen handling for unresolvable + // [JsonDerivedType] registrations. + ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason); continue; } @@ -1256,14 +1230,10 @@ private void InferClosedTypeDerivedTypes( if (GetEffectiveAccessibility(derivedType) < baseAccessibility) { - // The reflection path throws for inferred derived types less accessible than the - // base; mirror that by emitting a throwing factory instead of dropping the entry. + // The inferred derived type is less accessible than the base. Surface it at + // build time and drop the entry; source-gen reports invalid polymorphic + // configurations through diagnostics rather than baking runtime throws. ReportDiagnostic(DiagnosticDescriptors.InferredDerivedTypeIsNotAccessible, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); - unresolvedDerivedTypeError ??= string.Format( - CultureInfo.InvariantCulture, - InferredDerivedTypeIsNotAccessibleExceptionMessage, - derivedType.ToDisplayString(), - typeToGenerate.Type.ToDisplayString()); continue; } diff --git a/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs index dc3946eb142170..24c20a78ec1b35 100644 --- a/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PolymorphismOptionsSpec.cs @@ -26,15 +26,5 @@ public sealed record PolymorphismOptionsSpec public required string? TypeDiscriminatorPropertyName { get; init; } public required JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; init; } - - /// - /// When set, a registered derived type could not be resolved against this base type - /// (e.g. an open generic derived type that fails unification). The value is a - /// pre-formatted, non-localized message describing the failure. The emitter generates - /// a factory that - /// throws this message so the generated metadata fails the same way reflection does, - /// instead of silently serializing the base type non-polymorphically. - /// - public required string? UnresolvedDerivedTypeError { get; init; } } } From b870aed44b04ac5e79a1bdfffc95a3d0a764825b Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 29 Jun 2026 20:23:26 +0300 Subject: [PATCH 3/9] Don't force DefaultJsonTypeInfoResolver in closed-type inference tests The closed-type inference tests forced an explicit DefaultJsonTypeInfoResolver, which fails under the NativeAOT/source-gen test legs where reflection is disabled. Closed-type polymorphism inference is reflection-only, so rely on the ambient default reflection resolver instead and guard each test on JsonSerializer.IsReflectionEnabledByDefault so they no-op when reflection is unavailable. The forced resolver's stated rationale never applied: this file is only compiled into the reflection test project, not the source-gen one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PolymorphicTests.ClosedTypeInference.cs | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs index 4f63519ab6c294..7583d14f8ecb62 100644 --- a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs +++ b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NET11_0_OR_GREATER -using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; using Xunit; @@ -11,21 +10,25 @@ namespace System.Text.Json.Serialization.Tests public abstract partial class PolymorphicTests { // The closed-type hierarchies exercised below live in the IL fixture assembly - // System.Text.Json.ClosedTypeTestFixtures. They are annotated with - // [IsClosedType], which the C# compiler reserves for its own use, so the metadata - // cannot be authored in C#. These tests always run against the reflection-based - // resolver (even under source-gen subclasses) by supplying an explicit - // DefaultJsonTypeInfoResolver, so they validate the reflection inference path. + // System.Text.Json.ClosedTypeTestFixtures. They are annotated with [IsClosedType], + // which the C# compiler reserves for its own use, so the metadata cannot be authored + // in C#. Closed-type polymorphism inference is a reflection-only feature, so these + // tests rely on the default reflection-based resolver and are skipped when reflection + // is disabled (for example under source-gen subclasses or NativeAOT). private static JsonSerializerOptions CreateClosedTypeInferenceOptions(bool infer = true) => new JsonSerializerOptions { - TypeInfoResolver = new DefaultJsonTypeInfoResolver(), InferClosedTypePolymorphism = infer, }; [Fact] public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscriminator() { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); ClosedShape value = new ClosedCircle(); @@ -39,6 +42,11 @@ public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscrimina [Fact] public async Task ClosedTypeInference_FlagDisabled_DoesNotInferPolymorphism() { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(infer: false); ClosedShape value = new ClosedCircle(); @@ -50,6 +58,11 @@ public async Task ClosedTypeInference_FlagDisabled_DoesNotInferPolymorphism() [Fact] public async Task ClosedTypeInference_EmptyDerivedTypes_IsInert() { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); EmptyClosedBase value = new EmptyClosedDerived(); @@ -61,6 +74,11 @@ public async Task ClosedTypeInference_EmptyDerivedTypes_IsInert() [Fact] public async Task ClosedTypeInference_OpenGenericDerived_IsInferredAndResolved() { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); ClosedContainer value = new ClosedBox(); @@ -74,6 +92,11 @@ public async Task ClosedTypeInference_OpenGenericDerived_IsInferredAndResolved() [Fact] public async Task ClosedTypeInference_DuplicateDiscriminator_ThrowsInvalidOperationException() { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); ClosedCollisionBase value = new ClosedCollisionHolderA.Node(); @@ -83,6 +106,11 @@ public async Task ClosedTypeInference_DuplicateDiscriminator_ThrowsInvalidOperat [Fact] public async Task ClosedTypeInference_InaccessibleDerivedType_ThrowsInvalidOperationException() { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); // The closed hierarchy mixes a public and an internal derived type. The internal From 8b697256e6446cbb7dd019dc29473233e5fa1f57 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 29 Jun 2026 20:25:57 +0300 Subject: [PATCH 4/9] Correct IL fixture header comment to reflection-only The header claimed the closed-type fixtures serve both the reflection-based and source-generated serializers, but the fixture assembly is referenced only by System.Text.Json.Tests and the source-gen path does not support closed-type inference. Correct the comment to name the reflection-based tests as the sole consumer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ClosedTypeTestFixtures.il | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il index c776effb3af77d..6e7a8c4d927071 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il @@ -3,15 +3,15 @@ // This assembly provides closed-type hierarchies annotated with // System.Runtime.CompilerServices.IsClosedTypeAttribute. That attribute is reserved for -// compiler usage (applying it in C# source is a hard CS8335 error), and the 'closed' -// keyword that would make the C# compiler emit it is not yet available, so the metadata -// is hand-authored in IL here. It is consumed by closed-type polymorphism inference tests -// for both the reflection-based and source-generated serializers. +// compiler use (applying it in C# source is a hard CS8335 error), and the 'closed' keyword +// that would make the C# compiler emit it does not exist yet, so the metadata is +// hand-authored in IL here. It is consumed by the reflection-based closed-type polymorphism +// inference tests in System.Text.Json.Tests. // -// The IsClosedTypeAttribute custom-attribute blob uses a single named property -// 'DerivedTypes' of type System.Type[]. Types are intentionally property-less: the tests -// assert on the emitted '$type' discriminator and the deserialized runtime type, which is -// sufficient to prove that polymorphism was inferred. +// The IsClosedTypeAttribute blob carries a single named property 'DerivedTypes' of type +// System.Type[]. The fixture types are intentionally property-less: the tests assert on the +// emitted '$type' discriminator and the deserialized runtime type, which is sufficient to +// prove that polymorphism was inferred. .assembly extern System.Runtime { From 2c8de106c91b380bbb7e60fea5941c5a70953312 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 29 Jun 2026 20:40:12 +0300 Subject: [PATCH 5/9] Expand closed-type inference test coverage Exercise both derived types of the basic hierarchy via [Theory], and add tests for collection-element inference, nested closed-type properties alongside regular properties (with payload round-trip), and the unknown-discriminator deserialization failure path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PolymorphicTests.ClosedTypeInference.cs | 76 +++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs index 7583d14f8ecb62..39810adc34f232 100644 --- a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs +++ b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NET11_0_OR_GREATER +using System.Collections.Generic; using System.Threading.Tasks; using Xunit; @@ -21,8 +22,10 @@ private static JsonSerializerOptions CreateClosedTypeInferenceOptions(bool infer InferClosedTypePolymorphism = infer, }; - [Fact] - public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscriminator() + [Theory] + [InlineData(typeof(ClosedCircle), "ClosedCircle")] + [InlineData(typeof(ClosedSquare), "ClosedSquare")] + public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscriminator(Type derivedType, string expectedDiscriminator) { if (!JsonSerializer.IsReflectionEnabledByDefault) { @@ -31,12 +34,67 @@ public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscrimina JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); - ClosedShape value = new ClosedCircle(); + ClosedShape value = (ClosedShape)Activator.CreateInstance(derivedType)!; string json = await Serializer.SerializeWrapper(value, options); - JsonTestHelper.AssertJsonEqual("""{"$type":"ClosedCircle"}""", json); + JsonTestHelper.AssertJsonEqual($$"""{"$type":"{{expectedDiscriminator}}"}""", json); ClosedShape roundtripped = await Serializer.DeserializeWrapper(json, options); - Assert.IsType(roundtripped); + Assert.IsType(derivedType, roundtripped); + } + + [Fact] + public async Task ClosedTypeInference_CollectionOfClosedBase_InfersEachElement() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + List value = new() { new ClosedCircle(), new ClosedSquare() }; + string json = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual("""[{"$type":"ClosedCircle"},{"$type":"ClosedSquare"}]""", json); + + List roundtripped = await Serializer.DeserializeWrapper>(json, options); + Assert.Collection( + roundtripped, + element => Assert.IsType(element), + element => Assert.IsType(element)); + } + + [Fact] + public async Task ClosedTypeInference_NestedClosedProperty_InfersAlongsideRegularProperties() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + ClosedShapeHolder value = new() { Name = "holder", Shape = new ClosedSquare() }; + string json = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual("""{"Name":"holder","Shape":{"$type":"ClosedSquare"}}""", json); + + ClosedShapeHolder roundtripped = await Serializer.DeserializeWrapper(json, options); + Assert.Equal("holder", roundtripped.Name); + Assert.IsType(roundtripped.Shape); + } + + [Fact] + public async Task ClosedTypeInference_DeserializeUnknownDiscriminator_Throws() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + + // "Triangle" is not part of the closed ClosedShape hierarchy, so resolving it must fail. + await Assert.ThrowsAsync( + () => Serializer.DeserializeWrapper("""{"$type":"Triangle"}""", options)); } [Fact] @@ -119,5 +177,13 @@ public async Task ClosedTypeInference_InaccessibleDerivedType_ThrowsInvalidOpera await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, options)); } } + + // Regular POCO that holds a closed-type property; used to verify inference applies to + // nested closed values alongside ordinary properties. + public sealed class ClosedShapeHolder + { + public string? Name { get; set; } + public ClosedShape? Shape { get; set; } + } } #endif From cd4dcd481a9cdd6501ecf238ef8671434bfb2c34 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 15 Jul 2026 21:17:08 +0300 Subject: [PATCH 6/9] Refine closed hierarchy polymorphism inference Align reflection and source-generation validation, generic specialization, diagnostics, metadata inference, and test coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8967afcb-a170-497d-930a-a69e5ccf8773 --- .../gen/Helpers/KnownTypeSymbols.cs | 3 - .../gen/Helpers/RoslynExtensions.cs | 383 ++++++++ ...onSourceGenerator.DiagnosticDescriptors.cs | 6 +- ...urceGenerator.Emitter.ExceptionMessages.cs | 3 + .../gen/JsonSourceGenerator.Emitter.cs | 25 + .../gen/JsonSourceGenerator.Parser.cs | 275 +++--- .../gen/Model/TypeGenerationSpec.cs | 7 + .../gen/Resources/Strings.resx | 8 +- .../gen/Resources/xlf/Strings.cs.xlf | 12 +- .../gen/Resources/xlf/Strings.de.xlf | 12 +- .../gen/Resources/xlf/Strings.es.xlf | 12 +- .../gen/Resources/xlf/Strings.fr.xlf | 12 +- .../gen/Resources/xlf/Strings.it.xlf | 12 +- .../gen/Resources/xlf/Strings.ja.xlf | 12 +- .../gen/Resources/xlf/Strings.ko.xlf | 12 +- .../gen/Resources/xlf/Strings.pl.xlf | 12 +- .../gen/Resources/xlf/Strings.pt-BR.xlf | 12 +- .../gen/Resources/xlf/Strings.ru.xlf | 12 +- .../gen/Resources/xlf/Strings.tr.xlf | 12 +- .../gen/Resources/xlf/Strings.zh-Hans.xlf | 12 +- .../gen/Resources/xlf/Strings.zh-Hant.xlf | 12 +- .../src/System/ReflectionExtensions.cs | 312 +++++++ .../Serialization/JsonSerializerOptions.cs | 3 +- .../DefaultJsonTypeInfoResolver.Helpers.cs | 85 +- .../Metadata/JsonMetadataServices.Helpers.cs | 1 + .../PolymorphicTests.ClosedTypeInference.cs | 831 ++++++++++++++++-- .../PolymorphicTests.CustomTypeHierarchies.cs | 380 +++++++- .../ClosedTypeTestFixtures.il | 213 ----- ...em.Text.Json.ClosedTypeTestFixtures.ilproj | 13 - .../TestClasses.cs | 34 + .../PolymorphicTests.ClosedTypeInference.cs | 229 +++++ .../Serialization/PolymorphicTests.cs | 56 ++ ...m.Text.Json.SourceGeneration.Tests.targets | 3 + .../JsonSourceGeneratorDiagnosticsTests.cs | 339 +++++++ .../System.Text.Json.Tests.csproj | 8 +- 35 files changed, 2731 insertions(+), 642 deletions(-) delete mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il delete mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.ClosedTypeInference.cs diff --git a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs index 1e18dd675315d9..5b7d2c5d21f76f 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs @@ -261,9 +261,6 @@ public KnownTypeSymbols(Compilation compilation) public INamedTypeSymbol? UnsafeAccessorAttributeType => GetOrResolveType("System.Runtime.CompilerServices.UnsafeAccessorAttribute", ref _UnsafeAccessorAttributeType); private Option _UnsafeAccessorAttributeType; - public INamedTypeSymbol? IsClosedTypeAttributeType => GetOrResolveType("System.Runtime.CompilerServices.IsClosedTypeAttribute", ref _IsClosedTypeAttributeType); - private Option _IsClosedTypeAttributeType; - // OverloadResolutionPriorityAttribute was added in .NET 9; its presence indicates // the runtime also supports generic type parameters in UnsafeAccessor. public bool SupportsGenericUnsafeAccessors => UnsafeAccessorAttributeType is not null diff --git a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs index a730e09f2f43b6..7a5e8c3fae9f05 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -15,6 +16,8 @@ namespace System.Text.Json.SourceGeneration { internal static class RoslynExtensions { + private static readonly Func? s_isClosedTypeAccessor = CreateIsClosedTypeAccessor(); + public static LanguageVersion? GetLanguageVersion(this Compilation compilation) => compilation is CSharpCompilation csc ? csc.LanguageVersion : null; @@ -787,5 +790,385 @@ private static bool HasCodeAnalysisAttribute(this ISymbol symbol, string attribu attr.AttributeClass?.Name == attributeName && attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis"); } + + // Polyfill for the closed-type reflection APIs added to Roslyn in dotnet/roslyn#84045 + // (ITypeSymbol.IsClosed and ITypeSymbol.GetClosedDerivedTypeInfo, available from Roslyn 5.10). + // The generator compiles against Roslyn 4.4, so it cannot reference those APIs directly. These + // reconstruct the same information from APIs available in 4.4 so that closed-type polymorphism + // inference works on any compiler that understands the 'closed' modifier, without depending on + // whether Roslyn surfaces the reserved closed-type metadata to the symbol API (it currently + // filters the compiler-emitted [IsClosedType] attribute out of ITypeSymbol.GetAttributes()). + // The generator targets older Roslyn APIs, so the built-in APIs are accessed through light-up + // until the generator's Roslyn floor is raised past 5.10. + // + // Roslyn implementation at dotnet/roslyn @ 18bf2c8709264bac6615856e507eb44ba2a026e2: + // Public ITypeSymbol implementation: + // https://github.com/dotnet/roslyn/blob/18bf2c8709264bac6615856e507eb44ba2a026e2/src/Compilers/CSharp/Portable/Symbols/PublicModel/TypeSymbol.cs#L207-L220 + // Source-symbol candidate enumeration mirrored by these polyfills: + // https://github.com/dotnet/roslyn/blob/18bf2c8709264bac6615856e507eb44ba2a026e2/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs#L903-L961 + + /// + /// Polyfill for ITypeSymbol.IsClosed: returns when + /// is an abstract class declared with the closed modifier, whose + /// hierarchy the language restricts to its declaring module. Detection is syntactic because the + /// compiler-emitted [IsClosedType] attribute is filtered out of the symbol API. When + /// available, the built-in API is used through light-up so metadata-only symbols are supported; + /// otherwise source declarations fall back to syntax inspection. + /// + public static bool IsClosedType(this INamedTypeSymbol type) + { + if (type is not { TypeKind: TypeKind.Class, IsAbstract: true }) + { + return false; + } + + if (s_isClosedTypeAccessor is not null) + { + return s_isClosedTypeAccessor(type); + } + + foreach (SyntaxReference syntaxReference in type.DeclaringSyntaxReferences) + { + if (syntaxReference.GetSyntax() is BaseTypeDeclarationSyntax declaration) + { + foreach (SyntaxToken modifier in declaration.Modifiers) + { + // The 'closed' contextual keyword is tokenized as a dedicated SyntaxKind by + // closed-aware compilers, but that enum member does not exist in the Roslyn + // version the generator compiles against. Compare the token text instead, which + // is stable across compiler versions. + if (modifier.Text is "closed") + { + return true; + } + } + } + } + + return false; + } + + private static Func? CreateIsClosedTypeAccessor() + { + // IsClosed is unavailable in the Roslyn reference assemblies used to compile the generator. + MethodInfo? getter = typeof(ITypeSymbol).GetProperty("IsClosed")?.GetMethod; + return getter is null + ? null + : (Func)getter.CreateDelegate(typeof(Func)); + } + + /// + /// Polyfill for ITypeSymbol.GetClosedDerivedTypeInfo().ClosedDerivedTypes: reconstructs the + /// immediate derived types of a closed hierarchy. The closed modifier constrains subtyping + /// to the base type's declaring module, so the immediate derived types are exactly the same-module + /// named types whose direct base type shares 's original definition — + /// the set Roslyn records internally as CandidateClosedSubtypeDefinitions. Generic derived + /// types are returned in unbound form so callers can unify them against the constructed base. + /// Returns when none are found. Derived types are yielded in module-scan + /// order; callers that require a canonical ordering (for example, for deterministic generator + /// output) order them by discriminator, where uniqueness is established. + /// + public static List? GetClosedDerivedTypes(this INamedTypeSymbol closedType) + { + INamedTypeSymbol baseDefinition = closedType.OriginalDefinition; + List? derivedTypes = null; + + foreach (INamedTypeSymbol candidate in EnumerateNamedTypes(closedType.ContainingModule.GlobalNamespace)) + { + if (candidate.BaseType is { } candidateBase && + SymbolEqualityComparer.Default.Equals(candidateBase.OriginalDefinition, baseDefinition)) + { + ITypeSymbol derivedType = candidate.IsGenericType + ? candidate.ConstructUnboundGenericType() + : candidate; + + (derivedTypes ??= new()).Add(derivedType); + } + } + + return derivedTypes; + + static IEnumerable EnumerateNamedTypes(INamespaceSymbol namespaceSymbol) + { + foreach (INamespaceOrTypeSymbol member in namespaceSymbol.GetMembers()) + { + if (member is INamespaceSymbol childNamespace) + { + foreach (INamedTypeSymbol nestedType in EnumerateNamedTypes(childNamespace)) + { + yield return nestedType; + } + } + else if (member is INamedTypeSymbol namedType) + { + yield return namedType; + + foreach (INamedTypeSymbol nestedType in EnumerateNestedTypes(namedType)) + { + yield return nestedType; + } + } + } + } + + static IEnumerable EnumerateNestedTypes(INamedTypeSymbol type) + { + foreach (INamedTypeSymbol nestedType in type.GetTypeMembers()) + { + yield return nestedType; + + foreach (INamedTypeSymbol deeperType in EnumerateNestedTypes(nestedType)) + { + yield return deeperType; + } + } + } + } + + // The following is a faithful port of the Roslyn C# compiler's own accessibility comparison — the + // logic it runs to report the "inconsistent accessibility" diagnostics (CS0050/CS0060 and friends). + // Callers use it to decide whether one type is at least as visible as another — for example, whether + // an inferred closed-hierarchy derived type is at least as visible as the base it is registered under, + // so that every location that can reference the base can also reference the derived type. Rather than + // invent an accessibility metric (which would risk drifting from the language as new modifiers are + // added), we reproduce the compiler's algorithm verbatim, using its terminology, so it tracks C# + // accessibility as it evolves. The reflection resolver in DefaultJsonTypeInfoResolver.Helpers.cs + // mirrors this over System.Type. + // + // Ported from dotnet/roslyn @ 121e7dc868d26be12b9c3fb52b7b9d2ae41a1ac2: + // IsAtLeastAsVisibleAs / FindTypeLessVisibleThan / IsAsRestrictive: + // https://github.com/dotnet/roslyn/blob/121e7dc868d26be12b9c3fb52b7b9d2ae41a1ac2/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs#L1048 + // IsAccessibleViaInheritance: + // https://github.com/dotnet/roslyn/blob/121e7dc868d26be12b9c3fb52b7b9d2ae41a1ac2/src/Compilers/CSharp/Portable/Symbols/SymbolExtensions.cs#L48 + // HasInternalAccessTo: + // https://github.com/dotnet/roslyn/blob/121e7dc868d26be12b9c3fb52b7b9d2ae41a1ac2/src/Compilers/CSharp/Portable/Binder/Semantics/AccessCheck.cs#L676 + + /// + /// Determines whether is at least as visible as . Port + /// of Roslyn's TypeSymbolExtensions.IsAtLeastAsVisibleAs; because a closed hierarchy relates + /// two named types, the compound-type traversal (FindTypeLessVisibleThan/Symbol.VisitType) + /// reduces to the single IsAsRestrictive check. + /// + public static bool IsAtLeastAsVisibleAs(this ITypeSymbol type, ITypeSymbol sym) + { + return IsAsRestrictive(type, sym); + + static bool IsAsRestrictive(ISymbol s1, ISymbol sym2) + { + Accessibility acc1 = s1.DeclaredAccessibility; + + if (acc1 == Accessibility.Public) + { + return true; + } + + for (ISymbol s2 = sym2; s2.Kind != SymbolKind.Namespace; s2 = s2.ContainingSymbol!) + { + Accessibility acc2 = s2.DeclaredAccessibility; + + switch (acc1) + { + case Accessibility.Internal: + // If s2 is private or internal, and is in an assembly that gives s1's assembly + // internal access, then this is at least as restrictive as s1's internal. + if (acc2 is Accessibility.Private or Accessibility.Internal or Accessibility.ProtectedAndInternal && + HasInternalAccessTo(s2.ContainingAssembly, s1.ContainingAssembly)) + { + return true; + } + + break; + + case Accessibility.ProtectedAndInternal: + // Since s1 is private protected, s2 must be more restrictive than both internal and + // protected. Do the "internal" test first (as above); if it passes, fall through to + // the "protected" test. + if (acc2 is Accessibility.Private or Accessibility.Internal or Accessibility.ProtectedAndInternal && + HasInternalAccessTo(s2.ContainingAssembly, s1.ContainingAssembly)) + { + goto case Accessibility.Protected; + } + + break; + + case Accessibility.Protected: + { + INamedTypeSymbol? parent1 = s1.ContainingType; + + if (parent1 is null) + { + // not helpful + } + else if (acc2 == Accessibility.Private) + { + // if s2 is private and within s1's parent or within a subclass of s1's + // parent, then this is at least as restrictive as s1's protected. + for (INamedTypeSymbol? parent2 = s2.ContainingType; parent2 is not null; parent2 = parent2.ContainingType) + { + if (IsAccessibleViaInheritance(parent1, parent2)) + { + return true; + } + } + } + else if (acc2 is Accessibility.Protected or Accessibility.ProtectedAndInternal) + { + // if s2 is protected, and its parent is a subclass of (or the same as) s1's + // parent, then this is at least as restrictive as s1's protected. + INamedTypeSymbol? parent2 = s2.ContainingType; + if (parent2 is not null && IsAccessibleViaInheritance(parent1, parent2)) + { + return true; + } + } + + break; + } + + case Accessibility.ProtectedOrInternal: + { + INamedTypeSymbol? parent1 = s1.ContainingType; + + if (parent1 is null) + { + break; + } + + switch (acc2) + { + case Accessibility.Private: + // if s2 is private and within a subclass of s1's parent, or within the + // same assembly as s1, then this is at least as restrictive as s1's + // protected internal. + if (HasInternalAccessTo(s2.ContainingAssembly, s1.ContainingAssembly)) + { + return true; + } + + for (INamedTypeSymbol? parent2 = s2.ContainingType; parent2 is not null; parent2 = parent2.ContainingType) + { + if (IsAccessibleViaInheritance(parent1, parent2)) + { + return true; + } + } + + break; + + case Accessibility.Internal: + // If s2 is in an assembly that gives s1's assembly internal access, then + // this is more restrictive than s1's protected internal. + if (HasInternalAccessTo(s2.ContainingAssembly, s1.ContainingAssembly)) + { + return true; + } + + break; + + case Accessibility.Protected: + // if s2 is protected, and its parent is a subclass of (or the same as) + // s1's parent, then this is at least as restrictive as s1's protected internal. + if (s2.ContainingType is INamedTypeSymbol protectedParent2 && + IsAccessibleViaInheritance(parent1, protectedParent2)) + { + return true; + } + + break; + + case Accessibility.ProtectedAndInternal: + // if s2 is private protected, and its parent is a subclass of (or the same + // as) s1's parent, or it is in the same assembly as s1, then this is at + // least as restrictive as s1's protected internal. + if (HasInternalAccessTo(s2.ContainingAssembly, s1.ContainingAssembly) || + (s2.ContainingType is INamedTypeSymbol privateProtectedParent2 && + IsAccessibleViaInheritance(parent1, privateProtectedParent2))) + { + return true; + } + + break; + + case Accessibility.ProtectedOrInternal: + // if s2 is protected internal, and its parent is a subclass of (or the same + // as) s1's parent, and it is in the same assembly as s1, then this is at + // least as restrictive as s1's protected internal. + if (HasInternalAccessTo(s2.ContainingAssembly, s1.ContainingAssembly) && + s2.ContainingType is INamedTypeSymbol protectedOrInternalParent2 && + IsAccessibleViaInheritance(parent1, protectedOrInternalParent2)) + { + return true; + } + + break; + } + + break; + } + + case Accessibility.Private: + if (acc2 == Accessibility.Private) + { + // if s2 is private, and it is within s1's parent, then this is at least as + // restrictive as s1's private. + INamedTypeSymbol? parent1 = s1.ContainingType; + + if (parent1 is null) + { + break; + } + + INamedTypeSymbol parent1OriginalDefinition = parent1.OriginalDefinition; + for (INamedTypeSymbol? parent2 = s2.ContainingType; parent2 is not null; parent2 = parent2.ContainingType) + { + if (SymbolEqualityComparer.Default.Equals(parent2.OriginalDefinition, parent1OriginalDefinition)) + { + return true; + } + } + } + + break; + } + } + + return false; + } + + static bool IsAccessibleViaInheritance(INamedTypeSymbol superType, INamedTypeSymbol subType) + { + INamedTypeSymbol originalSuperType = superType.OriginalDefinition; + for (INamedTypeSymbol? current = subType; current is not null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, originalSuperType)) + { + return true; + } + } + + if (originalSuperType.TypeKind == TypeKind.Interface) + { + foreach (INamedTypeSymbol current in subType.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, originalSuperType)) + { + return true; + } + } + } + + return false; + } + + static bool HasInternalAccessTo(IAssemblySymbol fromAssembly, IAssemblySymbol toAssembly) + { + if (SymbolEqualityComparer.Default.Equals(fromAssembly, toAssembly)) + { + return true; + } + + return toAssembly.GivesAccessTo(fromAssembly); + } + } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs index fefe33a1edfae0..09b92d896b404d 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs @@ -164,10 +164,10 @@ internal static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static DiagnosticDescriptor InferredDerivedTypeDiscriminatorCollision { get; } = DiagnosticDescriptorHelper.Create( + public static DiagnosticDescriptor DerivedTypeDiscriminatorCollision { get; } = DiagnosticDescriptorHelper.Create( id: "SYSLIB1242", - title: new LocalizableResourceString(nameof(SR.InferredDerivedTypeDiscriminatorCollisionTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), - messageFormat: new LocalizableResourceString(nameof(SR.InferredDerivedTypeDiscriminatorCollisionMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + title: new LocalizableResourceString(nameof(SR.DerivedTypeDiscriminatorCollisionTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.DerivedTypeDiscriminatorCollisionMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), category: JsonConstants.SystemTextJsonSourceGenerationName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs index 35f34315bfc3ab..5649961b5c9a9b 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs @@ -16,6 +16,9 @@ private sealed partial class Emitter /// private static class ExceptionMessages { + public const string ClosedTypeInferenceRequiresCompileTimeOptIn = + "The 'JsonSerializerOptions.InferClosedTypePolymorphism' setting is enabled for the closed type '{0}', but its derived type metadata was not generated by the source generator. Enable 'JsonSourceGenerationOptionsAttribute.InferClosedTypePolymorphism' when generating the metadata, or register the derived types explicitly using 'JsonDerivedTypeAttribute'."; + public const string IncompatibleConverterType = "The converter '{0}' is not compatible with the type '{1}'."; diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 054858b398ce18..a6d9bbf360961a 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -449,6 +449,8 @@ private SourceText GenerateForCollection(ContextGenerationSpec contextSpec, Type jsonTypeInfo.NumberHandling = {{FormatNumberHandling(typeGenerationSpec.NumberHandling)}}; """); + GenerateClosedTypeInferenceGuard(writer, typeGenerationSpec); + GenerateTypeInfoFactoryFooter(writer); if (serializeMethodName != null) @@ -634,6 +636,8 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene } } + GenerateClosedTypeInferenceGuard(writer, typeMetadata); + GenerateTypeInfoFactoryFooter(writer); if (propInitMethodName != null) @@ -1925,6 +1929,27 @@ private static void GenerateTypeInfoFactoryFooter(SourceWriter writer) """); } + /// + /// Emits a runtime guard for closed hierarchies whose derived-type polymorphism metadata was not + /// generated because JsonSourceGenerationOptionsAttribute.InferClosedTypePolymorphism was + /// disabled at compile time. Enabling the setting only on the runtime JsonSerializerOptions + /// cannot recover that metadata, so we fail explicitly rather than silently serializing the base + /// type non-polymorphically. + /// + private static void GenerateClosedTypeInferenceGuard(SourceWriter writer, TypeGenerationSpec typeSpec) + { + if (typeSpec.IsClosedTypeWithoutInferredPolymorphism) + { + writer.WriteLine($$""" + + if (options.InferClosedTypePolymorphism) + { + throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.ClosedTypeInferenceRequiresCompileTimeOptIn}}", typeof({{typeSpec.TypeRef.FullyQualifiedName}}))); + } + """); + } + } + private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec, bool emitGetConverterForNullablePropertyMethod, bool emitValueTypeSetterDelegate, bool emitByteArrayValueHelper) { string contextTypeRef = contextSpec.ContextType.FullyQualifiedName; diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 8309edff903bfc..62442ea5f027c9 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -713,6 +713,7 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener out bool foundJsonConverterAttribute, out TypeRef? customConverterType, out PolymorphismOptionsSpec? polymorphismOptions, + out bool hasClosedDerivedTypes, out TypeRef? unionClassifierFactoryType); bool isPolymorphic = polymorphismOptions is not null; @@ -913,6 +914,10 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener PrimitiveTypeKind = primitiveTypeKind, IsPolymorphic = isPolymorphic, PolymorphismOptions = polymorphismOptions, + IsClosedTypeWithoutInferredPolymorphism = + polymorphismOptions is null && + options?.InferClosedTypePolymorphism is not true && + hasClosedDerivedTypes, NumberHandling = numberHandling, UnmappedMemberHandling = unmappedMemberHandling, PreferredPropertyObjectCreationHandling = preferredPropertyObjectCreationHandling, @@ -959,6 +964,7 @@ private void ProcessTypeCustomAttributes( out bool foundJsonConverterAttribute, out TypeRef? customConverterType, out PolymorphismOptionsSpec? polymorphismOptions, + out bool hasClosedDerivedTypes, out TypeRef? unionClassifierFactoryType) { numberHandling = null; @@ -969,6 +975,7 @@ private void ProcessTypeCustomAttributes( customConverterType = null; foundJsonConverterAttribute = false; polymorphismOptions = null; + hasClosedDerivedTypes = false; unionClassifierFactoryType = null; bool hasPolymorphicAttribute = false; @@ -977,8 +984,8 @@ private void ProcessTypeCustomAttributes( string? typeDiscriminatorPropertyName = null; TypeRef? polymorphicClassifierFactoryType = null; List? derivedTypes = null; + HashSet? typeDiscriminators = null; bool hasExplicitDerivedTypeAttribute = false; - ImmutableArray closedTypeDerivedTypes = default; bool hasUnionTypeClassifierSpecified = false; bool isUnionType = IsUnionType(typeToGenerate.Type); INamedTypeSymbol? namedUnionType = typeToGenerate.Type as INamedTypeSymbol; @@ -1049,37 +1056,7 @@ private void ProcessTypeCustomAttributes( { Debug.Assert(attributeData.ConstructorArguments.Length > 0); hasExplicitDerivedTypeAttribute = true; - var derivedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!; - - if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived) - { - if (!TryResolveOpenGenericDerivedType( - unboundDerived, typeToGenerate.Type, - out INamedTypeSymbol? resolvedType, out string? failureReason)) - { - // An open generic derived type that cannot be unified against this base - // has no concrete type to emit. Surface it at build time and drop the - // entry; source-gen reports invalid polymorphic configurations through - // diagnostics rather than baking runtime throws into generated metadata. - ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, attributeData.GetLocation(), derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason); - continue; - } - - derivedType = resolvedType!; - } - else if (!typeToGenerate.Type.IsAssignableFrom(derivedType)) - { - // A closed derived type that is not assignable to the base is not a - // supported polymorphic derived type. The shared runtime resolver already - // throws DerivedTypeNotSupported for the emitted entry; surface the problem - // at build time with a warning so it is not missed. - ReportDiagnostic(DiagnosticDescriptors.DerivedTypeIsNotSupported, attributeData.GetLocation(), derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); - } - - TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode); - - // The generated polymorphic metadata references the derived type by name. - AddExperimentalDiagnosticIds(derivedType, ref experimentalIds); + ITypeSymbol derivedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!; object? typeDiscriminator = null; if (attributeData.ConstructorArguments.Length == 2) @@ -1088,16 +1065,16 @@ private void ProcessTypeCustomAttributes( Debug.Assert(typeDiscriminator is int or string); } - if (derivedTypes is null && typeToGenerate.Mode == JsonSourceGenerationMode.Serialization) - { - ReportDiagnostic(DiagnosticDescriptors.PolymorphismNotSupported, typeToGenerate.Location, typeToGenerate.Type.ToDisplayString()); - } - - (derivedTypes ??= new()).Add(new DerivedTypeSpec - { - DerivedType = derivedTypeRef, - TypeDiscriminator = typeDiscriminator, - }); + AddPolymorphicDerivedType( + typeToGenerate, + derivedType, + typeDiscriminator, + attributeData.GetLocation(), + typeToGenerate.Location, + ref typeDiscriminators, + ref experimentalIds, + ref derivedTypes, + isInferredDerivedType: false); } else if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonPolymorphicAttributeType)) { @@ -1140,27 +1117,30 @@ namedUnionType is not null && } } } - else if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.IsClosedTypeAttributeType)) - { - foreach (KeyValuePair namedArg in attributeData.NamedArguments) - { - if (namedArg.Key == "DerivedTypes") - { - closedTypeDerivedTypes = namedArg.Value.Values; - break; - } - } - } } - // Infer polymorphism from a closed type hierarchy when InferClosedTypePolymorphism is - // enabled, the base type is marked [IsClosedType] with a non-empty derived type set, and - // no explicit [JsonDerivedType] attributes were specified (explicit registration wins). - if (options?.InferClosedTypePolymorphism is true && - !hasExplicitDerivedTypeAttribute && - !closedTypeDerivedTypes.IsDefaultOrEmpty) + // Enumerate a closed hierarchy once when it is needed either for inference or to determine + // whether generated metadata must reject runtime-only inference. Explicit derived-type + // registrations suppress inference, while any explicit polymorphism metadata makes the + // runtime-only inference guard unnecessary. + bool shouldInferClosedTypePolymorphism = + options?.InferClosedTypePolymorphism is true && !hasExplicitDerivedTypeAttribute; + bool needsRuntimeInferenceGuard = + options?.InferClosedTypePolymorphism is not true && + !hasPolymorphicAttribute && + derivedTypes is null; + + if ((shouldInferClosedTypePolymorphism || needsRuntimeInferenceGuard) && + typeToGenerate.Type is INamedTypeSymbol closedBaseType && + closedBaseType.IsClosedType()) { - InferClosedTypeDerivedTypes(typeToGenerate, closedTypeDerivedTypes, ref derivedTypes); + List? closedDerivedTypes = closedBaseType.GetClosedDerivedTypes(); + hasClosedDerivedTypes = closedDerivedTypes is { Count: > 0 }; + + if (shouldInferClosedTypePolymorphism && closedDerivedTypes is not null) + { + InferClosedTypeDerivedTypes(typeToGenerate, closedDerivedTypes, ref typeDiscriminators, ref experimentalIds, ref derivedTypes); + } } if (hasPolymorphicAttribute || derivedTypes is { Count: > 0 }) @@ -1183,109 +1163,114 @@ namedUnionType is not null && } } - /// - /// Synthesizes entries from the [IsClosedType] derived - /// type set, mirroring the reflection-side inference in - /// DefaultJsonTypeInfoResolver.Helpers.PopulatePolymorphismMetadata. Each inferred - /// entry uses the derived type's metadata name as its string discriminator. - /// - private void InferClosedTypeDerivedTypes( + private void AddPolymorphicDerivedType( in TypeToGenerate typeToGenerate, - ImmutableArray closedTypeDerivedTypes, - ref List? derivedTypes) + ITypeSymbol derivedType, + object? typeDiscriminator, + Location? derivedTypeDiagnosticLocation, + Location? polymorphismDiagnosticLocation, + ref HashSet? typeDiscriminators, + ref HashSet? experimentalIds, + ref List? derivedTypes, + bool isInferredDerivedType) { - int baseAccessibility = GetEffectiveAccessibility(typeToGenerate.Type); - HashSet? seenDiscriminators = null; + ITypeSymbol? resolvedDerivedType = derivedType; - foreach (TypedConstant closedTypeConstant in closedTypeDerivedTypes) + if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived) { - if (closedTypeConstant.Value is not ITypeSymbol closedDerivedType) + if (!TryResolveOpenGenericDerivedType( + unboundDerived, typeToGenerate.Type, + out resolvedDerivedType, out string? failureReason)) { - continue; + // An open generic derived type that cannot be unified against this base + // has no concrete type to emit. Surface it at build time and emit a + // guaranteed-invalid placeholder so the shared runtime resolver rejects + // the hierarchy, matching reflection without referencing the unresolvable type. + ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, derivedTypeDiagnosticLocation, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason); } + } - ITypeSymbol derivedType = closedDerivedType; + if (resolvedDerivedType is not null && + !typeToGenerate.Type.IsAssignableFrom(resolvedDerivedType)) + { + // The shared runtime resolver throws DerivedTypeNotSupported for the emitted entry; + // surface the problem at build time and keep the entry so runtime behavior matches. + ReportDiagnostic(DiagnosticDescriptors.DerivedTypeIsNotSupported, derivedTypeDiagnosticLocation, resolvedDerivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); + } - if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived) - { - if (!TryResolveOpenGenericDerivedType(unboundDerived, typeToGenerate.Type, out INamedTypeSymbol? resolvedType, out string? failureReason)) - { - // An inferred open generic derived type that cannot be unified against this - // base has no concrete type to emit. Surface it at build time and drop the - // entry, mirroring the established source-gen handling for unresolvable - // [JsonDerivedType] registrations. - ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason); - continue; - } + derivedType = resolvedDerivedType ?? _knownSymbols.ObjectType; - derivedType = resolvedType!; - } - else if (!typeToGenerate.Type.IsAssignableFrom(derivedType)) - { - // Not assignable to the base: the shared runtime resolver throws - // DerivedTypeNotSupported for the emitted entry, matching reflection. Surface the - // problem at build time and keep the entry so the runtime behavior matches. - ReportDiagnostic(DiagnosticDescriptors.DerivedTypeIsNotSupported, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); - } + // An inferred derived type must be at least as visible as the base type it is + // being registered under; otherwise there are call sites that can see the base but + // not the derived type, and the generated context could not reference it. + if (isInferredDerivedType && + !derivedType.IsAtLeastAsVisibleAs(typeToGenerate.Type)) + { + // Emit a guaranteed-invalid placeholder so the shared runtime resolver rejects + // the hierarchy without referencing the inaccessible type. + ReportDiagnostic(DiagnosticDescriptors.InferredDerivedTypeIsNotAccessible, derivedTypeDiagnosticLocation, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); + derivedType = _knownSymbols.ObjectType; + } - if (GetEffectiveAccessibility(derivedType) < baseAccessibility) - { - // The inferred derived type is less accessible than the base. Surface it at - // build time and drop the entry; source-gen reports invalid polymorphic - // configurations through diagnostics rather than baking runtime throws. - ReportDiagnostic(DiagnosticDescriptors.InferredDerivedTypeIsNotAccessible, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); - continue; - } + if (typeDiscriminator is not null && + !(typeDiscriminators ??= new()).Add(typeDiscriminator)) + { + // Emit the colliding entry anyway so the shared runtime resolver throws, + // matching the reflection path; surface the collision at build time. + ReportDiagnostic(DiagnosticDescriptors.DerivedTypeDiscriminatorCollision, derivedTypeDiagnosticLocation, typeDiscriminator, typeToGenerate.Type.ToDisplayString()); + } - string discriminator = closedDerivedType.MetadataName; - if (!(seenDiscriminators ??= new(StringComparer.Ordinal)).Add(discriminator)) - { - // Emit the colliding entry anyway so the shared runtime resolver throws, - // matching the reflection path; surface the collision at build time. - ReportDiagnostic(DiagnosticDescriptors.InferredDerivedTypeDiscriminatorCollision, typeToGenerate.Location, discriminator, typeToGenerate.Type.ToDisplayString()); - } + if (derivedTypes is null && typeToGenerate.Mode == JsonSourceGenerationMode.Serialization) + { + ReportDiagnostic(DiagnosticDescriptors.PolymorphismNotSupported, polymorphismDiagnosticLocation, typeToGenerate.Type.ToDisplayString()); + } - if (derivedTypes is null && typeToGenerate.Mode == JsonSourceGenerationMode.Serialization) - { - ReportDiagnostic(DiagnosticDescriptors.PolymorphismNotSupported, typeToGenerate.Location, typeToGenerate.Type.ToDisplayString()); - } + TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode); - TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode); - (derivedTypes ??= new()).Add(new DerivedTypeSpec - { - DerivedType = derivedTypeRef, - TypeDiscriminator = discriminator, - }); - } + // The generated polymorphic metadata references the derived type by name. + AddExperimentalDiagnosticIds(derivedType, ref experimentalIds); + + (derivedTypes ??= new()).Add(new DerivedTypeSpec + { + DerivedType = derivedTypeRef, + TypeDiscriminator = typeDiscriminator, + }); } /// - /// Computes the effective accessibility rank of a type for closed-hierarchy inference: - /// 2 = publicly visible, 1 = internal/assembly visible, 0 = private or protected. - /// The effective rank is the most restrictive level across the type's nesting chain. - /// Mirrors DefaultJsonTypeInfoResolver.Helpers.GetEffectiveAccessibility. + /// Synthesizes entries from a closed hierarchy's immediate + /// derived type set, mirroring the reflection-side inference in + /// DefaultJsonTypeInfoResolver.Helpers.PopulatePolymorphismMetadata. Each inferred + /// entry uses the derived type's simple name as its string discriminator. /// - private static int GetEffectiveAccessibility(ITypeSymbol type) + private void InferClosedTypeDerivedTypes( + in TypeToGenerate typeToGenerate, + List closedTypeDerivedTypes, + ref HashSet? typeDiscriminators, + ref HashSet? experimentalIds, + ref List? derivedTypes) { - int rank = 2; - - for (ITypeSymbol? current = type; current is not null; current = current.ContainingType) - { - int level = current.DeclaredAccessibility switch - { - Accessibility.Public => 2, - Accessibility.Internal or Accessibility.ProtectedOrInternal => 1, - Accessibility.Protected or Accessibility.ProtectedAndInternal or Accessibility.Private => 0, - _ => 2, - }; - - if (level < rank) - { - rank = level; - } + // Surface inference diagnostics at the [JsonSerializable] registration site when available + // (mirroring the TypeNotSupported handling), so they appear on the context the author + // controls rather than on the closed base type's declaration, which may live in another + // file or referenced assembly. + Location? diagnosticLocation = typeToGenerate.AttributeLocation ?? typeToGenerate.Location; + + // Order by the simple name used as the discriminator, which must be unique. + foreach (ITypeSymbol closedDerivedType in closedTypeDerivedTypes.OrderBy(static type => type.Name, StringComparer.Ordinal)) + { + string discriminator = closedDerivedType.Name; + AddPolymorphicDerivedType( + typeToGenerate, + closedDerivedType, + discriminator, + diagnosticLocation, + diagnosticLocation, + ref typeDiscriminators, + ref experimentalIds, + ref derivedTypes, + isInferredDerivedType: true); } - - return rank; } /// @@ -1307,7 +1292,7 @@ private static int GetEffectiveAccessibility(ITypeSymbol type) private bool TryResolveOpenGenericDerivedType( INamedTypeSymbol unboundDerived, ITypeSymbol baseType, - out INamedTypeSymbol? resolvedType, + [NotNullWhen(true)] out ITypeSymbol? resolvedType, out string? failureReason) { resolvedType = null; diff --git a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs index 383a002ce5af7f..4a4d21c3d30296 100644 --- a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs @@ -53,6 +53,13 @@ public sealed record TypeGenerationSpec public required PolymorphismOptionsSpec? PolymorphismOptions { get; init; } + /// + /// Indicates a closed type whose polymorphism metadata was not inferred at compile time + /// (because was not enabled). + /// Used to emit a marker so the runtime can fail fast if closed-type inference is requested there. + /// + public required bool IsClosedTypeWithoutInferredPolymorphism { get; init; } + public required bool IsValueTuple { get; init; } public required JsonNumberHandling? NumberHandling { get; init; } diff --git a/src/libraries/System.Text.Json/gen/Resources/Strings.resx b/src/libraries/System.Text.Json/gen/Resources/Strings.resx index 4f38b1aaea827b..35494412884c9e 100644 --- a/src/libraries/System.Text.Json/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/gen/Resources/Strings.resx @@ -231,11 +231,11 @@ The inferred derived type '{0}' is less accessible than the polymorphic base type '{1}'. Inferred derived types must be at least as accessible as the base type. - - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. the derived type is not assignable to the base type diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf index a43d040a35227e..7f05897f2fd54c 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf @@ -62,14 +62,14 @@ Atribut JsonDerivedTypeAttribute se v JsonSourceGenerationMode.Serialization nepodporuje. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf index d666f15f9c3d7e..3f0ecf0ba52438 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf @@ -62,14 +62,14 @@ „JsonDerivedTypeAttribute“ wird in „JsonSourceGenerationMode.Serialization“ nicht unterstützt. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf index bf1c82960b1bd8..38a3ca25ef2842 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf @@ -62,14 +62,14 @@ \"JsonDerivedTypeAttribute\" no se admite en \"JsonSourceGenerationMode.Serialization\". - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf index 64f8d4074ef17a..7883b64a2945a0 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf @@ -62,14 +62,14 @@ « JsonDerivedTypeAttribute » n’est pas pris en charge dans « JsonSourceGenerationMode.Serialization ». - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf index 8bbbab9ae8597f..cbdf2b77110146 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf @@ -62,14 +62,14 @@ 'JsonDerivedTypeAttribute' non è supportato in 'JsonSourceGenerationMode.Serialization'. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf index 4112589fb4729e..9315dc7ba0cbaf 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf @@ -62,14 +62,14 @@ 'JsonDerivedTypeAttribute' は 'JsonSourceGenerationMode.Serialization' ではサポートされていません。 - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf index f0373fbabbcfad..c47885e09eced7 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf @@ -62,14 +62,14 @@ 'JsonSourceGenerationMode.Serialization'에서는 'JsonDerivedTypeAttribute'가 지원되지 않습니다. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf index 3f61e2966733cf..5b5ab04d47d533 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf @@ -62,14 +62,14 @@ Atrybut „JsonDerivedTypeAttribute” nie jest obsługiwany w elemecie „JsonSourceGenerationMode.Serialization”. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf index dbf22e3a2de60d..70f74b1049f15f 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf @@ -62,14 +62,14 @@ 'JsonDerivedTypeAttribute' não tem suporte em 'JsonSourceGenerationMode.Serialization'. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf index 80e734c1cae370..ed21c51e87d05a 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf @@ -62,14 +62,14 @@ Атрибут JsonDerivedTypeAttribute не поддерживается в \"JsonSourceGenerationMode.Serialization\". - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf index fc8d873e840c59..e33d0a8206bcfd 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf @@ -62,14 +62,14 @@ 'JsonSourceGenerationMode.Serialization' içinde 'JsonDerivedTypeAttribute' desteklenmiyor. - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf index 0f5dd70f1453c6..fa6016a1b24e1f 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -62,14 +62,14 @@ \"JsonSourceGenerationMode.Serialization\" 中不支持 \"JsonDerivedTypeAttribute\"。 - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf index beb737cfc03c84..37d30d2f9b656a 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -62,14 +62,14 @@ 'JsonSourceGenerationMode.Serialization' 中不支援 'JsonDerivedTypeAttribute'。 - - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. - The inferred type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Inferred derived types must have unique names. + + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. + The type discriminator '{0}' is specified more than once for the polymorphic type '{1}'. Derived types must correspond to unique discriminators. - - Inferred derived types produce a duplicate type discriminator. - Inferred derived types produce a duplicate type discriminator. + + Derived types produce a duplicate type discriminator. + Derived types produce a duplicate type discriminator. diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs index 923ff3c00d4f92..7d86bb22a98888 100644 --- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs @@ -361,5 +361,317 @@ public static bool TryUnifyWith(this Type pattern, Type target, IDictionary + /// Determines whether is at least as visible as . Port of + /// Roslyn's TypeSymbolExtensions.IsAtLeastAsVisibleAs; because a closed hierarchy relates two + /// named types, the compound-type traversal (FindTypeLessVisibleThan/Symbol.VisitType) + /// reduces to the single IsAsRestrictive check. + /// + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + public static bool IsAtLeastAsVisibleAs(this Type type, Type sym) + { + return IsAsRestrictive(type, sym); + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + static bool IsAsRestrictive(Type s1, Type sym2) + { + Accessibility acc1 = GetDeclaredAccessibility(s1); + + if (acc1 == Accessibility.Public) + { + return true; + } + + for (Type? s2 = sym2; s2 is not null; s2 = s2.DeclaringType) + { + Accessibility acc2 = GetDeclaredAccessibility(s2); + + switch (acc1) + { + case Accessibility.Internal: + // If s2 is private or internal, and is in an assembly that gives s1's assembly internal + // access, then this is at least as restrictive as s1's internal. + if (acc2 is Accessibility.Private or Accessibility.Internal or Accessibility.ProtectedAndInternal && + HasInternalAccessTo(s2.Assembly, s1.Assembly)) + { + return true; + } + + break; + + case Accessibility.ProtectedAndInternal: + // Since s1 is private protected, s2 must be more restrictive than both internal and + // protected. Do the "internal" test first (as above); if it passes, fall through to the + // "protected" test. + if (acc2 is Accessibility.Private or Accessibility.Internal or Accessibility.ProtectedAndInternal && + HasInternalAccessTo(s2.Assembly, s1.Assembly)) + { + goto case Accessibility.Protected; + } + + break; + + case Accessibility.Protected: + { + Type? parent1 = s1.DeclaringType; + + if (parent1 is null) + { + // not helpful + } + else if (acc2 == Accessibility.Private) + { + // if s2 is private and within s1's parent or within a subclass of s1's parent, + // then this is at least as restrictive as s1's protected. + for (Type? parent2 = s2.DeclaringType; parent2 is not null; parent2 = parent2.DeclaringType) + { + if (IsAccessibleViaInheritance(parent1, parent2)) + { + return true; + } + } + } + else if (acc2 is Accessibility.Protected or Accessibility.ProtectedAndInternal) + { + // if s2 is protected, and its parent is a subclass of (or the same as) s1's + // parent, then this is at least as restrictive as s1's protected. + Type? parent2 = s2.DeclaringType; + if (parent2 is not null && IsAccessibleViaInheritance(parent1, parent2)) + { + return true; + } + } + + break; + } + + case Accessibility.ProtectedOrInternal: + { + Type? parent1 = s1.DeclaringType; + + if (parent1 is null) + { + break; + } + + switch (acc2) + { + case Accessibility.Private: + // if s2 is private and within a subclass of s1's parent, or within the same + // assembly as s1, then this is at least as restrictive as s1's protected internal. + if (HasInternalAccessTo(s2.Assembly, s1.Assembly)) + { + return true; + } + + for (Type? parent2 = s2.DeclaringType; parent2 is not null; parent2 = parent2.DeclaringType) + { + if (IsAccessibleViaInheritance(parent1, parent2)) + { + return true; + } + } + + break; + + case Accessibility.Internal: + // If s2 is in an assembly that gives s1's assembly internal access, then this + // is more restrictive than s1's protected internal. + if (HasInternalAccessTo(s2.Assembly, s1.Assembly)) + { + return true; + } + + break; + + case Accessibility.Protected: + // if s2 is protected, and its parent is a subclass of (or the same as) s1's + // parent, then this is at least as restrictive as s1's protected internal. + if (s2.DeclaringType is Type protectedParent2 && IsAccessibleViaInheritance(parent1, protectedParent2)) + { + return true; + } + + break; + + case Accessibility.ProtectedAndInternal: + // if s2 is private protected, and its parent is a subclass of (or the same as) + // s1's parent, or it is in the same assembly as s1, then this is at least as + // restrictive as s1's protected internal. + if (HasInternalAccessTo(s2.Assembly, s1.Assembly) || + (s2.DeclaringType is Type privateProtectedParent2 && IsAccessibleViaInheritance(parent1, privateProtectedParent2))) + { + return true; + } + + break; + + case Accessibility.ProtectedOrInternal: + // if s2 is protected internal, and its parent is a subclass of (or the same as) + // s1's parent, and it is in the same assembly as s1, then this is at least as + // restrictive as s1's protected internal. + if (HasInternalAccessTo(s2.Assembly, s1.Assembly) && + s2.DeclaringType is Type protectedOrInternalParent2 && IsAccessibleViaInheritance(parent1, protectedOrInternalParent2)) + { + return true; + } + + break; + } + + break; + } + + case Accessibility.Private: + if (acc2 == Accessibility.Private) + { + // if s2 is private, and it is within s1's parent, then this is at least as + // restrictive as s1's private. + Type? parent1 = s1.DeclaringType; + + if (parent1 is null) + { + break; + } + + Type parent1OriginalDefinition = OriginalDefinition(parent1); + for (Type? parent2 = s2.DeclaringType; parent2 is not null; parent2 = parent2.DeclaringType) + { + if (ReferenceEquals(OriginalDefinition(parent2), parent1OriginalDefinition)) + { + return true; + } + } + } + + break; + } + } + + return false; + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + static bool IsAccessibleViaInheritance(Type superType, Type subType) + { + Type originalSuperType = OriginalDefinition(superType); + for (Type? current = subType; current is not null; current = current.BaseType) + { + if (ReferenceEquals(OriginalDefinition(current), originalSuperType)) + { + return true; + } + } + + if (originalSuperType.IsInterface) + { + foreach (Type current in subType.GetInterfaces()) + { + if (ReferenceEquals(OriginalDefinition(current), originalSuperType)) + { + return true; + } + } + } + + return false; + } + + static bool HasInternalAccessTo(Assembly fromAssembly, Assembly toAssembly) + { + if (fromAssembly == toAssembly) + { + return true; + } + + // Reflection analog of Roslyn's AreInternalsVisibleToThisAssembly. Closed hierarchies are compiled + // into a single assembly, so this branch only matters for the general faithfulness of the port; the + // friend assembly is matched by simple name (the runtime already enforced strong-name identity when + // it loaded the types). + string? fromName = fromAssembly.GetName().Name; + foreach (CustomAttributeData attribute in toAssembly.GetCustomAttributesData()) + { + if (attribute.AttributeType == typeof(InternalsVisibleToAttribute) && + attribute.ConstructorArguments.Count > 0 && + attribute.ConstructorArguments[0].Value is string friendName) + { + int comma = friendName.IndexOf(','); + string friendSimpleName = comma < 0 ? friendName : friendName.Substring(0, comma); + if (string.Equals(friendSimpleName, fromName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + static Type OriginalDefinition(Type type) => + type.IsGenericType && !type.IsGenericTypeDefinition ? type.GetGenericTypeDefinition() : type; + + static Accessibility GetDeclaredAccessibility(Type type) + { + if (type.IsPublic || type.IsNestedPublic) + { + return Accessibility.Public; + } + + if (type.IsNestedFamORAssem) + { + return Accessibility.ProtectedOrInternal; // protected internal + } + + if (type.IsNestedFamily) + { + return Accessibility.Protected; + } + + if (type.IsNestedFamANDAssem) + { + return Accessibility.ProtectedAndInternal; // private protected + } + + if (type.IsNestedPrivate) + { + return Accessibility.Private; + } + + // Top-level non-public (IsNotPublic) and nested assembly (IsNestedAssembly) are both 'internal'. + return Accessibility.Internal; + } + } + + /// + /// Mirrors the members of Roslyn's that a C# type + /// declaration can have, so the ported accessibility comparison uses the compiler's terminology. + /// + private enum Accessibility + { + Private, + ProtectedAndInternal, // private protected + Protected, + Internal, + ProtectedOrInternal, // protected internal + Public, + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 530a3e858e529a..f0eccec6b27c9a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -887,7 +887,8 @@ public bool AllowDuplicateProperties /// By default, it's set to . When set to , types that /// declare a closed set of derived types (and that do not specify an explicit /// list) are treated as polymorphic, with one - /// derived type registered per member of the closed hierarchy. + /// derived type registered per member of the closed hierarchy. The simple name of each derived type, + /// equivalent to the result of nameof, is used as its type discriminator. /// public bool InferClosedTypePolymorphism { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index d59065f878b784..ae94576aa09270 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -87,28 +87,31 @@ internal static void PopulatePolymorphismMetadata(JsonTypeInfo typeInfo) JsonPolymorphismOptions? options = JsonPolymorphismOptions.CreateFromAttributeDeclarations(typeInfo.Type, out JsonPolymorphicAttribute? polymorphicAttribute); -#if NET11_0_OR_GREATER - // IsClosedTypeAttribute is a .NET 11 addition emitted by the compiler for closed - // hierarchies, so closed-type polymorphism inference is only possible on runtimes - // where the attribute type exists. On down-level targets the feature is a no-op. + // 'closed' is a C# language feature that can be used on any target framework, including .NET Framework. + // When targeting a runtime that predates System.Runtime.CompilerServices.IsClosedTypeAttribute, the C# + // compiler polyfills the attribute directly into the consuming assembly. We therefore detect it by full + // name (as we do for other compiler-emitted attributes such as RequiredMemberAttribute) instead of via a + // compile-time type reference, so that inference works on every target framework and regardless of whether + // the attribute is provided by the runtime or polyfilled by the compiler. if (typeInfo.Options.InferClosedTypePolymorphism && (options is null || options.DerivedTypes.Count == 0) && - typeInfo.Type.GetCustomAttribute(inherit: false) is { DerivedTypes.Length: > 0 } closedTypeAttribute) + GetInferredClosedDerivedTypes(typeInfo.Type) is { Length: > 0 } inferredDerivedTypes) { options ??= new(); - int baseAccessibility = GetEffectiveAccessibility(typeInfo.Type); - foreach (Type derivedType in closedTypeAttribute.DerivedTypes) + foreach (Type derivedType in inferredDerivedTypes) { - if (GetEffectiveAccessibility(derivedType) < baseAccessibility) + // An inferred derived type must be at least as visible as the base type it is + // being registered under; otherwise there are call sites that can see the base but + // not the derived type, and source-gen could not emit a reference to it. + if (!derivedType.IsAtLeastAsVisibleAs(typeInfo.Type)) { ThrowHelper.ThrowInvalidOperationException_InferredDerivedTypeIsNotAccessible(typeInfo.Type, derivedType); } - options.DerivedTypes.Add(new JsonDerivedType(derivedType, derivedType.Name)); + options.DerivedTypes.Add(new JsonDerivedType(derivedType, GetInferredTypeDiscriminator(derivedType))); } } -#endif if (options is not null) { @@ -134,6 +137,13 @@ internal static void PopulatePolymorphismMetadata(JsonTypeInfo typeInfo) typeInfo.TypeClassifierResolutionPending = true; } } + + static string GetInferredTypeDiscriminator(Type type) + { + string name = type.Name; + int genericAritySeparatorIndex = name.IndexOf('`'); + return genericAritySeparatorIndex < 0 ? name : name.Substring(0, genericAritySeparatorIndex); + } } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] @@ -179,41 +189,50 @@ private static void ResolveOpenGenericDerivedTypes(Type baseType, IList - /// Computes the effective accessibility rank of a type for closed-hierarchy inference: - /// 2 = publicly visible, 1 = internal/assembly visible, 0 = private or protected. - /// The effective rank is the most restrictive level across the type's nesting chain. + /// Reads the derived-type list from the compiler-emitted + /// System.Runtime.CompilerServices.IsClosedTypeAttribute that marks a closed type hierarchy. + /// The attribute is matched by full name because the C# compiler polyfills it into assemblies that + /// target runtimes without the type, making the polyfilled copy distinct from any runtime-provided one. + /// Returns when the type is not a closed hierarchy or declares no derived types. /// - private static int GetEffectiveAccessibility(Type type) + private static Type[]? GetInferredClosedDerivedTypes(Type type) { - int rank = 2; - - for (Type? current = type; current is not null; current = current.IsNested ? current.DeclaringType : null) + foreach (CustomAttributeData attributeData in type.GetCustomAttributesData()) { - int level; - if (current.IsPublic || current.IsNestedPublic) + Type attributeType = attributeData.AttributeType; + if (attributeType.Name != "IsClosedTypeAttribute" || + attributeType.FullName != "System.Runtime.CompilerServices.IsClosedTypeAttribute") { - level = 2; - } - else if (current.IsNestedFamily || current.IsNestedFamANDAssem || current.IsNestedPrivate) - { - level = 0; - } - else - { - level = 1; + continue; } - if (level < rank) + foreach (CustomAttributeNamedArgument namedArgument in attributeData.NamedArguments) { - rank = level; + if (namedArgument.MemberName == "DerivedTypes" && + namedArgument.TypedValue.Value is IList derivedTypeArguments) + { + Type[] derivedTypes = new Type[derivedTypeArguments.Count]; + for (int i = 0; i < derivedTypes.Length; i++) + { + if (derivedTypeArguments[i].Value is not Type derivedType) + { + return null; + } + + derivedTypes[i] = derivedType; + } + + return derivedTypes; + } } + + // The closed-type marker is present but carries no derived types. + return null; } - return rank; + return null; } -#endif /// /// Reflection-side resolver: closes against the diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs index d120a07b36cc9b..6fe3d6fd8a6750 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs @@ -94,6 +94,7 @@ private static JsonTypeInfo CreateCore( typeInfo.AddMethodDelegate = addFunc; typeInfo.SetCreateObjectIfCompatible(collectionInfo.ObjectCreator); PopulatePolymorphismMetadata(typeInfo, collectionInfo.PolymorphismOptions, collectionInfo.TypeClassifierFactory); + typeInfo.MapInterfaceTypesToCallbacks(); // Plug in any converter configuration -- should be run last. diff --git a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs index 39810adc34f232..54e643fd411301 100644 --- a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs +++ b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NET11_0_OR_GREATER using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; using Xunit; @@ -10,180 +11,820 @@ namespace System.Text.Json.Serialization.Tests { public abstract partial class PolymorphicTests { - // The closed-type hierarchies exercised below live in the IL fixture assembly - // System.Text.Json.ClosedTypeTestFixtures. They are annotated with [IsClosedType], - // which the C# compiler reserves for its own use, so the metadata cannot be authored - // in C#. Closed-type polymorphism inference is a reflection-only feature, so these - // tests rely on the default reflection-based resolver and are skipped when reflection - // is disabled (for example under source-gen subclasses or NativeAOT). - private static JsonSerializerOptions CreateClosedTypeInferenceOptions(bool infer = true) => - new JsonSerializerOptions + protected virtual JsonSerializerOptions ClosedTypeInferenceOptions => + field ??= new(Serializer.DefaultOptions) { - InferClosedTypePolymorphism = infer, + InferClosedTypePolymorphism = true, }; - [Theory] - [InlineData(typeof(ClosedCircle), "ClosedCircle")] - [InlineData(typeof(ClosedSquare), "ClosedSquare")] - public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscriminator(Type derivedType, string expectedDiscriminator) + private static string[] GetInferredDiscriminators(JsonSerializerOptions options, Type baseType) + { + JsonTypeInfo typeInfo = options.GetTypeInfo(baseType); + Assert.NotNull(typeInfo.PolymorphismOptions); + return typeInfo.PolymorphismOptions.DerivedTypes + .Select(derivedType => (string)derivedType.TypeDiscriminator!) + .OrderBy(discriminator => discriminator, StringComparer.Ordinal) + .ToArray(); + } + + public static IEnumerable BasicClosedHierarchyData() { - if (!JsonSerializer.IsReflectionEnabledByDefault) + yield return new object[] + { + new ClosedCircle { Name = "circle", Radius = 3 }, + """{"$type":"ClosedCircle","Radius":3,"Name":"circle"}""", + }; + yield return new object[] + { + new ClosedSquare { Name = "square", SideLength = 4 }, + """{"$type":"ClosedSquare","SideLength":4,"Name":"square"}""", + }; + yield return new object[] { - return; - } + new ClosedTriangle { Name = "triangle", BaseLength = 5, Height = 6 }, + """{"$type":"ClosedTriangle","BaseLength":5,"Height":6,"Name":"triangle"}""", + }; + } - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + [Theory] + [MemberData(nameof(BasicClosedHierarchyData))] + public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscriminator( + ClosedShape value, + string expectedJson) + { + JsonSerializerOptions options = ClosedTypeInferenceOptions; + Type expectedDerivedType = value.GetType(); - ClosedShape value = (ClosedShape)Activator.CreateInstance(derivedType)!; string json = await Serializer.SerializeWrapper(value, options); - JsonTestHelper.AssertJsonEqual($$"""{"$type":"{{expectedDiscriminator}}"}""", json); + JsonTestHelper.AssertJsonEqual(expectedJson, json); ClosedShape roundtripped = await Serializer.DeserializeWrapper(json, options); - Assert.IsType(derivedType, roundtripped); + Assert.IsType(expectedDerivedType, roundtripped); + + string roundtrippedJson = await Serializer.SerializeWrapper(roundtripped, options); + JsonTestHelper.AssertJsonEqual(expectedJson, roundtrippedJson); } [Fact] - public async Task ClosedTypeInference_CollectionOfClosedBase_InfersEachElement() + public void ClosedTypeInference_InferredDiscriminatorsMatchSimpleTypeName() { - if (!JsonSerializer.IsReflectionEnabledByDefault) - { - return; - } + Assert.Equal( + new[] { "ClosedCircle", "ClosedSquare", "ClosedTriangle" }, + GetInferredDiscriminators(ClosedTypeInferenceOptions, typeof(ClosedShape))); + Assert.Equal( + new[] { nameof(ClosedBag), nameof(ClosedBox) }, + GetInferredDiscriminators(ClosedTypeInferenceOptions, typeof(ClosedContainer))); + } - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + [Fact] + public async Task ClosedTypeInference_PreservesDerivedTypeProperties() + { + JsonSerializerOptions options = ClosedTypeInferenceOptions; + + ClosedPayload text = new ClosedTextPayload { Id = "text", Text = "hello" }; + string textJson = await Serializer.SerializeWrapper(text, options); + JsonTestHelper.AssertJsonEqual( + """{"$type":"ClosedTextPayload","Text":"hello","Id":"text"}""", + textJson); + ClosedPayload textRoundtripped = await Serializer.DeserializeWrapper(textJson, options); + ClosedTextPayload textResult = Assert.IsType(textRoundtripped); + Assert.Equal("text", textResult.Id); + Assert.Equal("hello", textResult.Text); + + ClosedPayload number = new ClosedNumberPayload { Id = "number", Number = 42 }; + string numberJson = await Serializer.SerializeWrapper(number, options); + JsonTestHelper.AssertJsonEqual( + """{"$type":"ClosedNumberPayload","Number":42,"Id":"number"}""", + numberJson); + ClosedPayload numberRoundtripped = await Serializer.DeserializeWrapper(numberJson, options); + ClosedNumberPayload numberResult = Assert.IsType(numberRoundtripped); + Assert.Equal("number", numberResult.Id); + Assert.Equal(42, numberResult.Number); + } - List value = new() { new ClosedCircle(), new ClosedSquare() }; + [Fact] + public async Task ClosedTypeInference_CollectionOfClosedBase_InfersEachElement() + { + JsonSerializerOptions options = ClosedTypeInferenceOptions; + + List value = + [ + new ClosedCircle { Name = "circle", Radius = 3 }, + new ClosedSquare { Name = "square", SideLength = 4 }, + new ClosedTriangle { Name = "triangle", BaseLength = 5, Height = 6 }, + ]; string json = await Serializer.SerializeWrapper(value, options); - JsonTestHelper.AssertJsonEqual("""[{"$type":"ClosedCircle"},{"$type":"ClosedSquare"}]""", json); + JsonTestHelper.AssertJsonEqual( + """ + [ + {"$type":"ClosedCircle","Radius":3,"Name":"circle"}, + {"$type":"ClosedSquare","SideLength":4,"Name":"square"}, + {"$type":"ClosedTriangle","BaseLength":5,"Height":6,"Name":"triangle"} + ] + """, + json); List roundtripped = await Serializer.DeserializeWrapper>(json, options); Assert.Collection( roundtripped, - element => Assert.IsType(element), - element => Assert.IsType(element)); + element => + { + ClosedCircle circle = Assert.IsType(element); + Assert.Equal("circle", circle.Name); + Assert.Equal(3, circle.Radius); + }, + element => + { + ClosedSquare square = Assert.IsType(element); + Assert.Equal("square", square.Name); + Assert.Equal(4, square.SideLength); + }, + element => + { + ClosedTriangle triangle = Assert.IsType(element); + Assert.Equal("triangle", triangle.Name); + Assert.Equal(5, triangle.BaseLength); + Assert.Equal(6, triangle.Height); + }); } [Fact] public async Task ClosedTypeInference_NestedClosedProperty_InfersAlongsideRegularProperties() { - if (!JsonSerializer.IsReflectionEnabledByDefault) - { - return; - } - - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + JsonSerializerOptions options = ClosedTypeInferenceOptions; - ClosedShapeHolder value = new() { Name = "holder", Shape = new ClosedSquare() }; + ClosedShapeHolder value = new() + { + Name = "holder", + Shape = new ClosedSquare { Name = "nested-square", SideLength = 4 }, + }; string json = await Serializer.SerializeWrapper(value, options); - JsonTestHelper.AssertJsonEqual("""{"Name":"holder","Shape":{"$type":"ClosedSquare"}}""", json); + JsonTestHelper.AssertJsonEqual( + """{"Name":"holder","Shape":{"$type":"ClosedSquare","SideLength":4,"Name":"nested-square"}}""", + json); ClosedShapeHolder roundtripped = await Serializer.DeserializeWrapper(json, options); Assert.Equal("holder", roundtripped.Name); - Assert.IsType(roundtripped.Shape); + ClosedSquare square = Assert.IsType(roundtripped.Shape); + Assert.Equal("nested-square", square.Name); + Assert.Equal(4, square.SideLength); } [Fact] public async Task ClosedTypeInference_DeserializeUnknownDiscriminator_Throws() { - if (!JsonSerializer.IsReflectionEnabledByDefault) - { - return; - } - - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); - - // "Triangle" is not part of the closed ClosedShape hierarchy, so resolving it must fail. await Assert.ThrowsAsync( - () => Serializer.DeserializeWrapper("""{"$type":"Triangle"}""", options)); + () => Serializer.DeserializeWrapper( + """{"$type":"Nonexistent"}""", + ClosedTypeInferenceOptions)); } [Fact] public async Task ClosedTypeInference_FlagDisabled_DoesNotInferPolymorphism() { - if (!JsonSerializer.IsReflectionEnabledByDefault) - { - return; - } - - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(infer: false); - - ClosedShape value = new ClosedCircle(); + JsonSerializerOptions options = Serializer.DefaultOptions; + ClosedShape value = new ClosedCircle { Name = "circle", Radius = 3 }; string json = await Serializer.SerializeWrapper(value, options); - Assert.DoesNotContain("$type", json); + JsonTestHelper.AssertJsonEqual("""{"Name":"circle"}""", json); + Assert.Null(options.GetTypeInfo(typeof(ClosedShape)).PolymorphismOptions); } [Fact] - public async Task ClosedTypeInference_EmptyDerivedTypes_IsInert() + public void ClosedTypeInference_EmptyDerivedTypes_IsInert() { - if (!JsonSerializer.IsReflectionEnabledByDefault) - { - return; - } + Assert.Null(ClosedTypeInferenceOptions.GetTypeInfo(typeof(ClosedEmptyBase)).PolymorphismOptions); + } - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + [Fact] + public async Task ClosedTypeInference_PlainAbstractClass_IsNotInferred() + { + JsonSerializerOptions options = ClosedTypeInferenceOptions; + Assert.Null(options.GetTypeInfo(typeof(PlainAbstractBase)).PolymorphismOptions); - EmptyClosedBase value = new EmptyClosedDerived(); + PlainAbstractBase value = new PlainAbstractDerived(); string json = await Serializer.SerializeWrapper(value, options); - Assert.DoesNotContain("$type", json); } - [Fact] - public async Task ClosedTypeInference_OpenGenericDerived_IsInferredAndResolved() + public static IEnumerable GenericClosedHierarchyData() { - if (!JsonSerializer.IsReflectionEnabledByDefault) + yield return new object[] + { + typeof(ClosedContainer), + new ClosedBox { BaseValue = "base-string", Value = "x" }, + """{"$type":"ClosedBox","Value":"x","BaseValue":"base-string"}""", + typeof(ClosedBox), + }; + yield return new object[] + { + typeof(ClosedContainer), + new ClosedBox { BaseValue = 7, Value = 42 }, + """{"$type":"ClosedBox","Value":42,"BaseValue":7}""", + typeof(ClosedBox), + }; + yield return new object[] + { + typeof(ClosedContainer), + new ClosedBag { BaseValue = 3, Items = new() { 1, 2 } }, + """{"$type":"ClosedBag","Items":[1,2],"BaseValue":3}""", + typeof(ClosedBag), + }; + yield return new object[] + { + typeof(ClosedContainer>), + new ClosedBox> + { + BaseValue = new() { 4, 5 }, + Value = new() { 1, 2, 3 }, + }, + """{"$type":"ClosedBox","Value":[1,2,3],"BaseValue":[4,5]}""", + typeof(ClosedBox>), + }; + yield return new object[] + { + typeof(ClosedPair), + new ClosedEntry + { + BaseKey = "base", + BaseValue = 5, + Key = "k", + Value = 7, + }, + """{"$type":"ClosedEntry","Key":"k","Value":7,"BaseKey":"base","BaseValue":5}""", + typeof(ClosedEntry), + }; + yield return new object[] + { + typeof(ClosedWrappedBase>), + new ClosedWrappedDerived + { + BaseValue = new() { "base" }, + Data = new() { "a", "b" }, + }, + """{"$type":"ClosedWrappedDerived","Data":["a","b"],"BaseValue":["base"]}""", + typeof(ClosedWrappedDerived), + }; + yield return new object[] + { + typeof(ClosedArrayBase), + new ClosedArrayDerived + { + BaseValue = new[] { 4, 5 }, + Values = new[] { 1, 2, 3 }, + }, + """{"$type":"ClosedArrayDerived","Values":[1,2,3],"BaseValue":[4,5]}""", + typeof(ClosedArrayDerived), + }; + yield return new object[] + { + typeof(ClosedUnspeakableBase), + new ClosedUnspeakableIdentityDerived(), + """{"$type":"ClosedUnspeakableIdentityDerived"}""", + typeof(ClosedUnspeakableIdentityDerived), + }; + yield return new object[] + { + typeof(ClosedUnspeakableBase), + new ClosedUnspeakableArrayDerived(), + """{"$type":"ClosedUnspeakableArrayDerived"}""", + typeof(ClosedUnspeakableArrayDerived), + }; + yield return new object[] + { + typeof(ClosedReorderedBase), + new ClosedReorderedDerived + { + BaseFirst = 7, + BaseSecond = "base", + Left = "left", + Right = 42, + }, + """{"$type":"ClosedReorderedDerived","Left":"left","Right":42,"BaseFirst":7,"BaseSecond":"base"}""", + typeof(ClosedReorderedDerived), + }; + yield return new object[] + { + typeof(ClosedPartialBase), + new ClosedPartialDerived + { + BaseFirst = "base", + BaseSecond = 11, + Value = "hello", + }, + """{"$type":"ClosedPartialDerived","Value":"hello","BaseFirst":"base","BaseSecond":11}""", + typeof(ClosedPartialDerived), + }; + yield return new object[] + { + typeof(ClosedKvpBase>), + new ClosedKvpDerived + { + BaseValue = new("base", 5), + Pair = new("k", 99), + }, + """{"$type":"ClosedKvpDerived","Pair":{"Key":"k","Value":99},"BaseValue":{"Key":"base","Value":5}}""", + typeof(ClosedKvpDerived), + }; + yield return new object[] + { + typeof(ClosedTupleBase<(int, string)>), + new ClosedTupleDerived { BaseMarker = "base", Label = "pair" }, + """{"$type":"ClosedTupleDerived","Label":"pair","BaseMarker":"base"}""", + typeof(ClosedTupleDerived), + }; + yield return new object[] + { + typeof(ClosedNestedArgBase.NestedBox>), + new ClosedNestedArgDerived { BaseMarker = "base", Marker = "nested" }, + """{"$type":"ClosedNestedArgDerived","Marker":"nested","BaseMarker":"base"}""", + typeof(ClosedNestedArgDerived), + }; + yield return new object[] + { + typeof(ClosedConstrainedBase>), + new ClosedConstrainedDerived> + { + BaseValue = new() { "base" }, + Items = new() { "hello" }, + }, + """{"$type":"ClosedConstrainedDerived","Items":["hello"],"BaseValue":["base"]}""", + typeof(ClosedConstrainedDerived>), + }; + yield return new object[] + { + typeof(ClosedNestedDerivedBase), + new ClosedNestedDerivedBase.Derived { BaseValue = 7, Value = 42 }, + $$"""{"$type":"{{typeof(ClosedNestedDerivedBase.Derived).Name}}","Value":42,"BaseValue":7}""", + typeof(ClosedNestedDerivedBase.Derived), + }; + yield return new object[] + { + typeof(ClosedMixedBase), + new ClosedMixedOpenDerived { BaseValue = 1, Marker = "open" }, + """{"$type":"ClosedMixedOpenDerived","Marker":"open","BaseValue":1}""", + typeof(ClosedMixedOpenDerived), + }; + yield return new object[] + { + typeof(ClosedMixedBase), + new ClosedMixedFixedDerived { BaseValue = 2, Marker = "fixed" }, + """{"$type":"ClosedMixedFixedDerived","Marker":"fixed","BaseValue":2}""", + typeof(ClosedMixedFixedDerived), + }; + yield return new object[] + { + typeof(ClosedDeepJaggedBase>), + new ClosedDeepJaggedDerived { BaseMarker = "base", Marker = "deep" }, + """{"$type":"ClosedDeepJaggedDerived","Marker":"deep","BaseMarker":"base"}""", + typeof(ClosedDeepJaggedDerived), + }; + yield return new object[] { - return; - } + typeof(ClosedRepeatedBase), + new ClosedRepeatedDerived { First = 1, Second = 2, Marker = "repeated" }, + """{"$type":"ClosedRepeatedDerived","Marker":"repeated","First":1,"Second":2}""", + typeof(ClosedRepeatedDerived), + }; + } + + [Theory] + [MemberData(nameof(GenericClosedHierarchyData))] + public async Task ClosedTypeInference_GenericHierarchy_ResolvesAndRoundTripsDerivedType( + Type baseType, + object value, + string expectedJson, + Type expectedDerivedType) + { + JsonSerializerOptions options = ClosedTypeInferenceOptions; + string json = await Serializer.SerializeWrapper(value, baseType, options); + JsonTestHelper.AssertJsonEqual(expectedJson, json); - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + object roundtripped = await Serializer.DeserializeWrapper(json, baseType, options); + Assert.IsType(expectedDerivedType, roundtripped); - ClosedContainer value = new ClosedBox(); - string json = await Serializer.SerializeWrapper(value, options); - JsonTestHelper.AssertJsonEqual("""{"$type":"ClosedBox`1"}""", json); + string roundtrippedJson = await Serializer.SerializeWrapper(roundtripped, baseType, options); + JsonTestHelper.AssertJsonEqual(expectedJson, roundtrippedJson); + } + + public static IEnumerable InvalidGenericClosedHierarchyData() + { + yield return new object[] + { + typeof(ClosedGroundMismatchBase), + new ClosedGroundMismatchFallback { Marker = "ground" }, + }; + yield return new object[] + { + typeof(ClosedRepeatedMismatchBase), + new ClosedRepeatedMismatchFallback { Marker = "repeated" }, + }; + yield return new object[] + { + typeof(ClosedConstraintViolationBase), + new ClosedConstraintViolationFallback { Marker = "constraint" }, + }; + yield return new object[] + { + typeof(ClosedUnspeakableBase), + new ClosedUnspeakableIdentityDerived(), + }; + yield return new object[] + { + typeof(ClosedNestedMismatchBase.NestedBox>), + new ClosedNestedMismatchFallback { Marker = "nested-mismatch" }, + }; + yield return new object[] + { + typeof(ClosedDeepJaggedMismatchBase>), + new ClosedDeepJaggedMismatchFallback { Marker = "deep-mismatch" }, + }; + yield return new object[] + { + typeof(ClosedDuplicateArityBase), + new ClosedDuplicateArityDerived(), + }; + } + + [Theory] + [MemberData(nameof(InvalidGenericClosedHierarchyData))] + public async Task ClosedTypeInference_UnresolvableGenericDerivedType_ThrowsInvalidOperationException( + Type baseType, + object value) + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, baseType, ClosedTypeInferenceOptions)); + } - ClosedContainer roundtripped = await Serializer.DeserializeWrapper>(json, options); - Assert.IsType>(roundtripped); + [Fact] + public async Task ClosedTypeInference_ConstructedGenericSiblingNotAssignable_ThrowsInvalidOperationException() + { + ClosedConcreteMismatchBase value = new ClosedConcreteMismatchStringDerived(); + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, ClosedTypeInferenceOptions)); } [Fact] public async Task ClosedTypeInference_DuplicateDiscriminator_ThrowsInvalidOperationException() { - if (!JsonSerializer.IsReflectionEnabledByDefault) + ClosedCollisionBase value = new ClosedCollisionHolderA.Node(); + InvalidOperationException exception = await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, ClosedTypeInferenceOptions)); + Assert.Contains(nameof(ClosedCollisionHolderA.Node), exception.Message); + } + + [Fact] + public async Task ClosedTypeInference_DuplicateGenericNameAcrossArities_ThrowsInvalidOperationException() + { + ClosedDuplicateArityBase value = new ClosedDuplicateArityDerived(); + InvalidOperationException exception = await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, ClosedTypeInferenceOptions)); + Assert.Contains(nameof(ClosedDuplicateArityDerived), exception.Message); + } + + public static IEnumerable InaccessibleClosedHierarchyData() + { + yield return new object[] + { + typeof(ClosedAccessBase), + new ClosedAccessiblePublicDerived(), + }; + yield return new object[] + { + typeof(ClosedNestedAccessContainer.Base), + new ClosedNestedAccessContainer.KeptDerived(), + }; + yield return new object[] { - return; - } + typeof(ClosedProtectedAccessBase), + new ClosedProtectedAccessibleDerived(), + }; + } + + [Theory] + [MemberData(nameof(InaccessibleClosedHierarchyData))] + public async Task ClosedTypeInference_InaccessibleDerivedType(Type baseType, object value) + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, baseType, ClosedTypeInferenceOptions)); + } - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + [Fact] + public async Task ClosedTypeInference_ExplicitJsonDerivedType_SuppressesInference() + { + JsonSerializerOptions options = ClosedTypeInferenceOptions; + Assert.Equal( + new[] { "customA", "customB" }, + GetInferredDiscriminators(options, typeof(ClosedExplicitBase))); - ClosedCollisionBase value = new ClosedCollisionHolderA.Node(); - await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, options)); + ClosedExplicitBase value = new ClosedExplicitA { BaseValue = "base", DerivedValue = 42 }; + string json = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual( + """{"$type":"customA","DerivedValue":42,"BaseValue":"base"}""", + json); + + ClosedExplicitBase roundtripped = + await Serializer.DeserializeWrapper(json, options); + ClosedExplicitA result = Assert.IsType(roundtripped); + Assert.Equal("base", result.BaseValue); + Assert.Equal(42, result.DerivedValue); } [Fact] - public async Task ClosedTypeInference_InaccessibleDerivedType_ThrowsInvalidOperationException() + public async Task ClosedTypeInference_JsonPolymorphicAttribute_HonorsCustomDiscriminatorName() { - if (!JsonSerializer.IsReflectionEnabledByDefault) + ClosedCustomDiscriminatorBase value = new ClosedCustomDiscriminatorDerived { - return; - } + BaseValue = "base", + DerivedValue = 42, + }; + string json = await Serializer.SerializeWrapper(value, ClosedTypeInferenceOptions); + JsonTestHelper.AssertJsonEqual( + """{"$kind":"ClosedCustomDiscriminatorDerived","DerivedValue":42,"BaseValue":"base"}""", + json); + + ClosedCustomDiscriminatorBase roundtripped = + await Serializer.DeserializeWrapper( + json, + ClosedTypeInferenceOptions); + ClosedCustomDiscriminatorDerived result = + Assert.IsType(roundtripped); + Assert.Equal("base", result.BaseValue); + Assert.Equal(42, result.DerivedValue); + } + } + + public closed class ClosedShape + { + public string? Name { get; set; } + } + public sealed class ClosedCircle : ClosedShape + { + public int Radius { get; set; } + } + public sealed class ClosedSquare : ClosedShape + { + public int SideLength { get; set; } + } + public sealed class ClosedTriangle : ClosedShape + { + public int BaseLength { get; set; } + public int Height { get; set; } + } + + public closed class ClosedPayload + { + public string? Id { get; set; } + } + public sealed class ClosedTextPayload : ClosedPayload { public string? Text { get; set; } } + public sealed class ClosedNumberPayload : ClosedPayload { public int Number { get; set; } } + + public closed class ClosedEmptyBase; + + public abstract class PlainAbstractBase; + public sealed class PlainAbstractDerived : PlainAbstractBase; + + public closed class ClosedContainer + { + public T? BaseValue { get; set; } + } + public sealed class ClosedBox : ClosedContainer { public T? Value { get; set; } } + public sealed class ClosedBag : ClosedContainer { public List? Items { get; set; } } + + public closed class ClosedPair + { + public TKey? BaseKey { get; set; } + public TValue? BaseValue { get; set; } + } + public sealed class ClosedEntry : ClosedPair + { + public TKey? Key { get; set; } + public TValue? Value { get; set; } + } + + public closed class ClosedWrappedBase + { + public T? BaseValue { get; set; } + } + public sealed class ClosedWrappedDerived : ClosedWrappedBase> + { + public List? Data { get; set; } + } - JsonSerializerOptions options = CreateClosedTypeInferenceOptions(); + public closed class ClosedArrayBase + { + public T? BaseValue { get; set; } + } + public sealed class ClosedArrayDerived : ClosedArrayBase + { + public T[]? Values { get; set; } + } + + public closed class ClosedReorderedBase + { + public T1? BaseFirst { get; set; } + public T2? BaseSecond { get; set; } + } + public sealed class ClosedReorderedDerived : ClosedReorderedBase + { + public T1? Left { get; set; } + public T2? Right { get; set; } + } + + public closed class ClosedPartialBase + { + public T1? BaseFirst { get; set; } + public T2? BaseSecond { get; set; } + } + public sealed class ClosedPartialDerived : ClosedPartialBase + { + public T? Value { get; set; } + } + + public closed class ClosedKvpBase + { + public T? BaseValue { get; set; } + } + public sealed class ClosedKvpDerived : ClosedKvpBase> + { + public KeyValuePair Pair { get; set; } + } + + public closed class ClosedTupleBase + { + public string? BaseMarker { get; set; } + } + public sealed class ClosedTupleDerived : ClosedTupleBase<(T1, T2)> + { + public string? Label { get; set; } + } + + public sealed class ClosedNestedOuter + { + public sealed class NestedBox; + } + + public closed class ClosedNestedArgBase + { + public string? BaseMarker { get; set; } + } + public sealed class ClosedNestedArgDerived : ClosedNestedArgBase.NestedBox> + { + public string? Marker { get; set; } + } - // The closed hierarchy mixes a public and an internal derived type. The internal - // type is less accessible than the public base, so inference must reject it. - PublicClosedBase value = new PublicClosedDerived(); - await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, options)); + public closed class ClosedConstrainedBase + { + public T? BaseValue { get; set; } + } + public sealed class ClosedConstrainedDerived : ClosedConstrainedBase + where T : IEnumerable + { + public T? Items { get; set; } + } + + public closed class ClosedNestedDerivedBase + { + public T? BaseValue { get; set; } + + public sealed class Derived : ClosedNestedDerivedBase + { + public T? Value { get; set; } } } - // Regular POCO that holds a closed-type property; used to verify inference applies to - // nested closed values alongside ordinary properties. + public closed class ClosedMixedBase + { + public T? BaseValue { get; set; } + } + public sealed class ClosedMixedOpenDerived : ClosedMixedBase + { + public string? Marker { get; set; } + } + public sealed class ClosedMixedFixedDerived : ClosedMixedBase + { + public string? Marker { get; set; } + } + + public closed class ClosedGroundMismatchBase; + public sealed class ClosedGroundMismatchDerived : ClosedGroundMismatchBase; + public sealed class ClosedGroundMismatchFallback : ClosedGroundMismatchBase + { + public string? Marker { get; set; } + } + + public closed class ClosedRepeatedBase + { + public T1? First { get; set; } + public T2? Second { get; set; } + } + public sealed class ClosedRepeatedDerived : ClosedRepeatedBase + { + public string? Marker { get; set; } + } + public closed class ClosedRepeatedMismatchBase; + public sealed class ClosedRepeatedMismatchDerived : ClosedRepeatedMismatchBase; + public sealed class ClosedRepeatedMismatchFallback : ClosedRepeatedMismatchBase + { + public string? Marker { get; set; } + } + + public closed class ClosedConstraintViolationBase; + public sealed class ClosedConstraintViolationDerived : ClosedConstraintViolationBase + where T : struct; + public sealed class ClosedConstraintViolationFallback : ClosedConstraintViolationBase + { + public string? Marker { get; set; } + } + + public closed class ClosedNestedMismatchBase; + public sealed class ClosedNestedMismatchDerived : + ClosedNestedMismatchBase.NestedBox>; + public sealed class ClosedNestedMismatchFallback : + ClosedNestedMismatchBase.NestedBox> + { + public string? Marker { get; set; } + } + + public closed class ClosedDeepJaggedBase + { + public string? BaseMarker { get; set; } + } + public sealed class ClosedDeepJaggedDerived : ClosedDeepJaggedBase> + { + public string? Marker { get; set; } + } + + public closed class ClosedDeepJaggedMismatchBase; + public sealed class ClosedDeepJaggedMismatchDerived : ClosedDeepJaggedMismatchBase>; + public sealed class ClosedDeepJaggedMismatchFallback : ClosedDeepJaggedMismatchBase> + { + public string? Marker { get; set; } + } + + public closed class ClosedDuplicateArityBase; + public sealed class ClosedDuplicateArityDerived : ClosedDuplicateArityBase; + public sealed class ClosedDuplicateArityDerived : ClosedDuplicateArityBase; + + public closed class ClosedUnspeakableBase; + public sealed class ClosedUnspeakableIdentityDerived : ClosedUnspeakableBase; + public sealed class ClosedUnspeakableArrayDerived : ClosedUnspeakableBase; + + public closed class ClosedConcreteMismatchBase; + public sealed class ClosedConcreteMismatchIntDerived : ClosedConcreteMismatchBase; + public sealed class ClosedConcreteMismatchStringDerived : ClosedConcreteMismatchBase; + + public closed class ClosedCollisionBase; + public static class ClosedCollisionHolderA { public sealed class Node : ClosedCollisionBase; } + public static class ClosedCollisionHolderB { public sealed class Node : ClosedCollisionBase; } + + public closed class ClosedAccessBase; + public sealed class ClosedAccessiblePublicDerived : ClosedAccessBase; + internal sealed class ClosedAccessInternalDerived : ClosedAccessBase; + + public closed class ClosedProtectedAccessBase; + public sealed class ClosedProtectedAccessibleDerived : ClosedProtectedAccessBase; + + public class ClosedProtectedAccessContainer + { + protected sealed class HiddenDerived : ClosedProtectedAccessBase; + } + + public class ClosedNestedAccessContainer + { + protected internal closed class Base; + protected internal sealed class KeptDerived : Base; + internal sealed class DroppedDerived : Base; + } + + [JsonDerivedType(typeof(ClosedExplicitA), "customA")] + [JsonDerivedType(typeof(ClosedExplicitB), "customB")] + public closed class ClosedExplicitBase + { + public string? BaseValue { get; set; } + } + public sealed class ClosedExplicitA : ClosedExplicitBase + { + public int DerivedValue { get; set; } + } + public sealed class ClosedExplicitB : ClosedExplicitBase + { + public int DerivedValue { get; set; } + } + + [JsonPolymorphic(TypeDiscriminatorPropertyName = "$kind")] + public closed class ClosedCustomDiscriminatorBase + { + public string? BaseValue { get; set; } + } + public sealed class ClosedCustomDiscriminatorDerived : ClosedCustomDiscriminatorBase + { + public int DerivedValue { get; set; } + } + public sealed class ClosedShapeHolder { public string? Name { get; set; } public ClosedShape? Shape { get; set; } } } -#endif diff --git a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs index cdc813768e29c2..68ed6cf3a5fb39 100644 --- a/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs +++ b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.CustomTypeHierarchies.cs @@ -2637,7 +2637,9 @@ public async Task PolymorphicClasWithDuplicateTypeDiscriminators_ThrowsInvalidOp } [JsonDerivedType(typeof(A), "duplicateId")] +#pragma warning disable SYSLIB1242 // The duplicate discriminator is intentional for this test. [JsonDerivedType(typeof(B), "duplicateId")] +#pragma warning restore SYSLIB1242 public class PolymorphicClasWithDuplicateTypeDiscriminators { public class A : PolymorphicClasWithDuplicateTypeDiscriminators { } @@ -2663,19 +2665,16 @@ public class DerivedClass : PolymorphicGenericClass } } - [ConditionalFact] + [Fact] public async Task PolymorphicDerivedGenericClass_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - PolymorphicDerivedGenericClass value = new PolymorphicDerivedGenericClass.DerivedClass(); await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(DerivedClass<>))] +#pragma warning restore SYSLIB1229 public class PolymorphicDerivedGenericClass { public class DerivedClass : PolymorphicDerivedGenericClass @@ -2819,6 +2818,44 @@ public class OpenGenericBase_Wrapped public class OpenGenericDerived_Wrapped : OpenGenericBase_Wrapped>; + [Fact] + public async Task OpenGenericDerivedType_DeepJaggedTypeArgCompatibleSpecialization_Works() + { + OpenGenericDeepJaggedBase> value = + new OpenGenericDeepJaggedDerived { BaseMarker = "base", Marker = "deep" }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual( + """{"$type":"derived","Marker":"deep","BaseMarker":"base"}""", + json); + + OpenGenericDeepJaggedBase> result = + await Serializer.DeserializeWrapper>>(json); + OpenGenericDeepJaggedDerived derived = Assert.IsType>(result); + Assert.Equal("base", derived.BaseMarker); + Assert.Equal("deep", derived.Marker); + } + + [Fact] + public async Task OpenGenericDerivedType_DeepJaggedTypeArgIncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericDeepJaggedBase>(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericDeepJaggedDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericDeepJaggedBase + { + public string? BaseMarker { get; set; } + } + + public class OpenGenericDeepJaggedDerived : OpenGenericDeepJaggedBase> + { + public string? Marker { get; set; } + } + [Fact] public async Task OpenGenericDerivedType_Interface_Works() { @@ -2854,38 +2891,32 @@ public async Task OpenGenericDerivedType_DifferentTypeArguments_ProduceDifferent JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Value":"hello"}""", strJson); } - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_NonGenericBase_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - var value = new NonGenericBaseWithOpenGenericDerived(); await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(NonGenericBaseWithOpenGenericDerived.OpenDerived<>), "derived")] +#pragma warning restore SYSLIB1229 public class NonGenericBaseWithOpenGenericDerived { public class OpenDerived : NonGenericBaseWithOpenGenericDerived; } - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_TypeArgsNotResolvable_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Derived : Base - T cannot be determined from Base var value = new OpenGenericBase_Unresolvable(); await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericDerived_Unresolvable<>), "derived")] +#pragma warning restore SYSLIB1229 public class OpenGenericBase_Unresolvable { public T? Value { get; set; } @@ -2893,14 +2924,9 @@ public class OpenGenericBase_Unresolvable public class OpenGenericDerived_Unresolvable : OpenGenericBase_Unresolvable; - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_GroundMismatchAgainstClosedBase_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // OpenGenericDerived_GroundMismatch : OpenGenericBase_GroundMismatch // registered on OpenGenericBase_GroundMismatch. // Position 0 (T) unifies with int, but position 1 (concrete int in derived's base @@ -2911,7 +2937,9 @@ public async Task OpenGenericDerivedType_GroundMismatchAgainstClosedBase_ThrowsI await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericDerived_GroundMismatch<>), "derived")] +#pragma warning restore SYSLIB1229 public class OpenGenericBase_GroundMismatch; public class OpenGenericDerived_GroundMismatch : OpenGenericBase_GroundMismatch; @@ -3208,14 +3236,9 @@ public class OpenGenericDerived_Tuple : OpenGenericBase_Tuple<(T1, T2)> public (T1, T2) Pair { get; set; } } - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_AmbiguousInterfaceMatch_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Impl : IBase, IBase> registered on IBase>. // Both ancestors unify (T=List via the first interface, T=int via the second). // Result: ambiguous, throws. @@ -3223,44 +3246,40 @@ public async Task OpenGenericDerivedType_AmbiguousInterfaceMatch_ThrowsInvalidOp await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericImpl_Ambiguous<>), "impl")] +#pragma warning restore SYSLIB1229 public interface IOpenGenericBase_Ambiguous; public class OpenGenericImpl_Ambiguous : IOpenGenericBase_Ambiguous, IOpenGenericBase_Ambiguous>; - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_UnboundParameter_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Derived : Base — T2 is unspeakable (not bound by the base type's args). var value = new OpenGenericBase_Unbound(); await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericDerived_Unbound<,>), "derived")] +#pragma warning restore SYSLIB1229 public class OpenGenericBase_Unbound; public class OpenGenericDerived_Unbound : OpenGenericBase_Unbound; - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_ConstraintViolation_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Derived : Base where T : struct, registered on Base. // Constraint fails → InvalidOperationException. var value = new OpenGenericBase_StructConstraint(); await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericDerived_StructConstraint<>), "derived")] +#pragma warning restore SYSLIB1229 public class OpenGenericBase_StructConstraint; public class OpenGenericDerived_StructConstraint : OpenGenericBase_StructConstraint @@ -3418,6 +3437,274 @@ public class OpenGenericImpl_MultiCtor : OpenGenericImpl_MultiCtor_IntBase, I public T? Item { get; set; } } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GenericSpecialization_ConcreteDerivedTypeForDifferentConstruction_ThrowsInvalidOperationException(bool useStringSpecialization) + { + if (useStringSpecialization) + { + GenericSpecializationAnimal value = new GenericSpecializationDog { Value = "dog", Breed = "collie" }; + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + else + { + GenericSpecializationAnimal value = new GenericSpecializationCat { Value = 42, Lives = 9 }; + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + } + +#pragma warning disable SYSLIB1240 + [JsonDerivedType(typeof(GenericSpecializationCat), "cat")] + [JsonDerivedType(typeof(GenericSpecializationDog), "dog")] +#pragma warning restore SYSLIB1240 + public class GenericSpecializationAnimal + { + public T? Value { get; set; } + } + + public sealed class GenericSpecializationCat : GenericSpecializationAnimal + { + public int Lives { get; set; } + } + + public sealed class GenericSpecializationDog : GenericSpecializationAnimal + { + public string? Breed { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_InterfaceConstraintCompatibleSpecialization_Works() + { + OpenGenericInterfaceConstraintBase> value = + new OpenGenericInterfaceConstraintDerived> { Marker = "valid", Value = [1, 2] }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid","Value":[1,2]}""", json); + + OpenGenericInterfaceConstraintBase> result = + await Serializer.DeserializeWrapper>>(json); + OpenGenericInterfaceConstraintDerived> derived = + Assert.IsType>>(result); + Assert.Equal("valid", derived.Marker); + Assert.Equal([1, 2], derived.Value); + } + + [Theory] + [InlineData(InvalidInterfaceConstraintSpecialization.ListOfString)] + [InlineData(InvalidInterfaceConstraintSpecialization.EnumerableOfString)] + [InlineData(InvalidInterfaceConstraintSpecialization.ValueType)] + public async Task OpenGenericDerivedType_InterfaceConstraintIncompatibleSpecialization_ThrowsInvalidOperationException( + InvalidInterfaceConstraintSpecialization specialization) + { + Func serialize = specialization switch + { + InvalidInterfaceConstraintSpecialization.ListOfString => + () => Serializer.SerializeWrapper(new OpenGenericInterfaceConstraintBase>()), + InvalidInterfaceConstraintSpecialization.EnumerableOfString => + () => Serializer.SerializeWrapper(new OpenGenericInterfaceConstraintBase>()), + InvalidInterfaceConstraintSpecialization.ValueType => + () => Serializer.SerializeWrapper(new OpenGenericInterfaceConstraintBase()), + _ => throw new ArgumentOutOfRangeException(nameof(specialization)), + }; + + await Assert.ThrowsAsync(serialize); + } + + public enum InvalidInterfaceConstraintSpecialization + { + ListOfString, + EnumerableOfString, + ValueType, + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericInterfaceConstraintDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericInterfaceConstraintBase + { + public T? Value { get; set; } + } + + public class OpenGenericInterfaceConstraintDerived : OpenGenericInterfaceConstraintBase + where T : IEnumerable + { + public string? Marker { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_MixedValidAndUnboundRegistrations_ThrowsInvalidOperationException() + { + OpenGenericMultiArityBase value = new OpenGenericMultiArityDerived { Value = 42 }; + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(OpenGenericMultiArityDerived<>), "one")] +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericMultiArityDerived<,>), "two")] +#pragma warning restore SYSLIB1229 + public class OpenGenericMultiArityBase + { + public T? Value { get; set; } + } + + public class OpenGenericMultiArityDerived : OpenGenericMultiArityBase; + + public class OpenGenericMultiArityDerived : OpenGenericMultiArityBase; + + [Fact] + public async Task OpenGenericDerivedType_SameNameDifferentArities_ThrowsOnDiscriminatorCollision() + { + OpenGenericNameCollisionBase value = new OpenGenericNameCollisionDerived(); + InvalidOperationException exception = await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value)); + Assert.Contains(nameof(OpenGenericNameCollisionDerived), exception.Message); + } + + [Fact] + public async Task OpenGenericDerivedType_SameNameDifferentArities_IncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericNameCollisionBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType( + typeof(OpenGenericNameCollisionDerived<>), + nameof(OpenGenericNameCollisionDerived))] +#pragma warning restore SYSLIB1229 +#pragma warning disable SYSLIB1242 // The duplicate discriminator is intentional for this test. + [JsonDerivedType( + typeof(OpenGenericNameCollisionDerived<,>), + nameof(OpenGenericNameCollisionDerived))] +#pragma warning restore SYSLIB1242 + public class OpenGenericNameCollisionBase; + + public class OpenGenericNameCollisionDerived : OpenGenericNameCollisionBase; + + public class OpenGenericNameCollisionDerived : OpenGenericNameCollisionBase; + + [Fact] + public async Task OpenGenericDerivedType_RepeatedTypeParameterCompatibleSpecialization_Works() + { + OpenGenericRepeatedBase value = + new OpenGenericRepeatedDerived { First = 1, Second = 2, Marker = "valid" }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid","First":1,"Second":2}""", json); + + OpenGenericRepeatedBase result = + await Serializer.DeserializeWrapper>(json); + OpenGenericRepeatedDerived derived = Assert.IsType>(result); + Assert.Equal("valid", derived.Marker); + Assert.Equal(1, derived.First); + Assert.Equal(2, derived.Second); + } + + [Fact] + public async Task OpenGenericDerivedType_RepeatedTypeParameterIncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericRepeatedBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericRepeatedDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericRepeatedBase + { + public T1? First { get; set; } + public T2? Second { get; set; } + } + + public class OpenGenericRepeatedDerived : OpenGenericRepeatedBase + { + public string? Marker { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_FixedArgumentCompatibleSpecialization_Works() + { + OpenGenericFixedArgumentBase value = + new OpenGenericFixedArgumentDerived { First = "value", Second = 42, Marker = "valid" }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid","First":"value","Second":42}""", json); + + OpenGenericFixedArgumentBase result = + await Serializer.DeserializeWrapper>(json); + OpenGenericFixedArgumentDerived derived = + Assert.IsType>(result); + Assert.Equal("valid", derived.Marker); + Assert.Equal("value", derived.First); + Assert.Equal(42, derived.Second); + } + + [Fact] + public async Task OpenGenericDerivedType_FixedArgumentIncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericFixedArgumentBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericFixedArgumentDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericFixedArgumentBase + { + public T1? First { get; set; } + public T2? Second { get; set; } + } + + public class OpenGenericFixedArgumentDerived : OpenGenericFixedArgumentBase + { + public string? Marker { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_NewConstraintCompatibleSpecialization_Works() + { + OpenGenericNewConstraintBase value = + new OpenGenericNewConstraintDerived { Marker = "valid" }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid"}""", json); + + OpenGenericNewConstraintBase result = + await Serializer.DeserializeWrapper>(json); + Assert.IsType>(result); + } + + [Fact] + public async Task OpenGenericDerivedType_NewConstraintIncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericNewConstraintBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericNewConstraintDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericNewConstraintBase; + + public class OpenGenericNewConstraintDerived : OpenGenericNewConstraintBase + where T : class, new() + { + public string? Marker { get; set; } + } + + public class NewConstraintArgument; + + public class NoDefaultConstructorConstraintArgument + { + public NoDefaultConstructorConstraintArgument(int value) + { + Value = value; + } + + public int Value { get; } + } + #endregion #region Generic Variance Tests @@ -3533,14 +3820,9 @@ public async Task Variance_BivariantInterface_BothViaVariance_DefaultThrows() // behavior. The reflection side has always handled these cases correctly because // Type.GetGenericArguments() returns enclosing+leaf args together. - [ConditionalFact] + [Fact] public async Task NestedGeneric_EnclosingMismatch_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Pattern: NestedDerivedEnclosingMismatch : NestedBase.NestedBox>. // Target: NestedBase.NestedBox>. // The enclosing argument differs (int vs string) so unification MUST fail. The @@ -3552,7 +3834,9 @@ public async Task NestedGeneric_EnclosingMismatch_ThrowsInvalidOperationExceptio await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(NestedDerivedEnclosingMismatch<>), "nested")] +#pragma warning restore SYSLIB1229 public class NestedBase { public T? Item { get; set; } } public class NestedOuter { public class NestedBox { public TInner? Inner { get; set; } } } public class NestedDerivedEnclosingMismatch : NestedBase.NestedBox>; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il deleted file mode 100644 index 6e7a8c4d927071..00000000000000 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/ClosedTypeTestFixtures.il +++ /dev/null @@ -1,213 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// This assembly provides closed-type hierarchies annotated with -// System.Runtime.CompilerServices.IsClosedTypeAttribute. That attribute is reserved for -// compiler use (applying it in C# source is a hard CS8335 error), and the 'closed' keyword -// that would make the C# compiler emit it does not exist yet, so the metadata is -// hand-authored in IL here. It is consumed by the reflection-based closed-type polymorphism -// inference tests in System.Text.Json.Tests. -// -// The IsClosedTypeAttribute blob carries a single named property 'DerivedTypes' of type -// System.Type[]. The fixture types are intentionally property-less: the tests assert on the -// emitted '$type' discriminator and the deserialized runtime type, which is sufficient to -// prove that polymorphism was inferred. - -.assembly extern System.Runtime -{ - .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A) -} - -.assembly System.Text.Json.ClosedTypeTestFixtures -{ - .ver 1:0:0:0 -} - -.module System.Text.Json.ClosedTypeTestFixtures.dll - -.namespace System.Text.Json.Serialization.Tests -{ - // ---- Basic closed hierarchy ---------------------------------------------------------- - .class public abstract auto ansi beforefieldinit ClosedShape - extends [System.Runtime]System.Object - { - .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { - property type[] 'DerivedTypes' = type[2](System.Text.Json.Serialization.Tests.ClosedCircle System.Text.Json.Serialization.Tests.ClosedSquare) - } - .method family hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void [System.Runtime]System.Object::.ctor() - ret - } - } - - .class public auto ansi sealed beforefieldinit ClosedCircle - extends System.Text.Json.Serialization.Tests.ClosedShape - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void System.Text.Json.Serialization.Tests.ClosedShape::.ctor() - ret - } - } - - .class public auto ansi sealed beforefieldinit ClosedSquare - extends System.Text.Json.Serialization.Tests.ClosedShape - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void System.Text.Json.Serialization.Tests.ClosedShape::.ctor() - ret - } - } - - // ---- Empty closed hierarchy (inference must stay inert) ------------------------------ - .class public abstract auto ansi beforefieldinit EmptyClosedBase - extends [System.Runtime]System.Object - { - .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = ( - 01 00 00 00 - ) - .method family hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void [System.Runtime]System.Object::.ctor() - ret - } - } - - .class public auto ansi sealed beforefieldinit EmptyClosedDerived - extends System.Text.Json.Serialization.Tests.EmptyClosedBase - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void System.Text.Json.Serialization.Tests.EmptyClosedBase::.ctor() - ret - } - } - - // ---- Open generic closed hierarchy --------------------------------------------------- - .class public abstract auto ansi beforefieldinit ClosedContainer`1 - extends [System.Runtime]System.Object - { - .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { - property type[] 'DerivedTypes' = type[1](System.Text.Json.Serialization.Tests.ClosedBox`1) - } - .method family hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void [System.Runtime]System.Object::.ctor() - ret - } - } - - .class public auto ansi sealed beforefieldinit ClosedBox`1 - extends class System.Text.Json.Serialization.Tests.ClosedContainer`1 - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void class System.Text.Json.Serialization.Tests.ClosedContainer`1::.ctor() - ret - } - } - - // ---- Discriminator collision (two derived types share the simple name 'Node') -------- - .class public abstract auto ansi beforefieldinit ClosedCollisionBase - extends [System.Runtime]System.Object - { - .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { - property type[] 'DerivedTypes' = type[2](System.Text.Json.Serialization.Tests.ClosedCollisionHolderA/Node System.Text.Json.Serialization.Tests.ClosedCollisionHolderB/Node) - } - .method family hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void [System.Runtime]System.Object::.ctor() - ret - } - } - - .class public abstract auto ansi sealed beforefieldinit ClosedCollisionHolderA - extends [System.Runtime]System.Object - { - .class nested public auto ansi sealed beforefieldinit Node - extends System.Text.Json.Serialization.Tests.ClosedCollisionBase - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void System.Text.Json.Serialization.Tests.ClosedCollisionBase::.ctor() - ret - } - } - } - - .class public abstract auto ansi sealed beforefieldinit ClosedCollisionHolderB - extends [System.Runtime]System.Object - { - .class nested public auto ansi sealed beforefieldinit Node - extends System.Text.Json.Serialization.Tests.ClosedCollisionBase - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void System.Text.Json.Serialization.Tests.ClosedCollisionBase::.ctor() - ret - } - } - } - - // ---- Accessibility rejection (public base, internal derived) ------------------------- - .class public abstract auto ansi beforefieldinit PublicClosedBase - extends [System.Runtime]System.Object - { - .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsClosedTypeAttribute::.ctor() = { - property type[] 'DerivedTypes' = type[2](System.Text.Json.Serialization.Tests.PublicClosedDerived System.Text.Json.Serialization.Tests.InternalClosedDerived) - } - .method family hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void [System.Runtime]System.Object::.ctor() - ret - } - } - - .class public auto ansi sealed beforefieldinit PublicClosedDerived - extends System.Text.Json.Serialization.Tests.PublicClosedBase - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void System.Text.Json.Serialization.Tests.PublicClosedBase::.ctor() - ret - } - } - - .class private auto ansi sealed beforefieldinit InternalClosedDerived - extends System.Text.Json.Serialization.Tests.PublicClosedBase - { - .method public hidebysig specialname rtspecialname instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void System.Text.Json.Serialization.Tests.PublicClosedBase::.ctor() - ret - } - } -} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj b/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj deleted file mode 100644 index c0b067c31f183b..00000000000000 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.ClosedTypeTestFixtures/System.Text.Json.ClosedTypeTestFixtures.ilproj +++ /dev/null @@ -1,13 +0,0 @@ - - - System.Text.Json.ClosedTypeTestFixtures - 1.0.0.0 - - $(NetCoreAppCurrent) - disable - Microsoft - - - - - diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs index 4879565121d9ce..aed010244b79ed 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs @@ -20,8 +20,42 @@ public class ClassFromOtherAssemblyWithNonPublicMembers internal protected int InternalProtectedValue { get; set; } = 32; } + public closed class ReferencedClosedShape + { + } + + public sealed class ReferencedClosedCircle : ReferencedClosedShape + { + } + + public sealed class ReferencedClosedSquare : ReferencedClosedShape + { + } + [JsonSerializable(typeof(MyPoco))] public partial class NETStandardSerializerContext : JsonSerializerContext { } } + +namespace System.Runtime.CompilerServices +{ + [System.AttributeUsage(System.AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : System.Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + + public bool IsOptional { get; set; } + } + + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + internal sealed class IsClosedTypeAttribute : System.Attribute + { + public System.Type[] DerivedTypes { get; set; } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.ClosedTypeInference.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.ClosedTypeInference.cs new file mode 100644 index 00000000000000..2b894ef92cea74 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.ClosedTypeInference.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Serialization.Tests; +using System.Text.Json.SourceGeneration.Tests.NETStandard; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public sealed partial class PolymorphicTests_Metadata + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Metadata.Default.Options; + + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedNumberList))] + [JsonSerializable(typeof(ClosedShape))] + internal sealed partial class PolymorphicTestsContext_Metadata; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RuntimeInferClosedTypePolymorphism_EmptyHierarchy_IsInert(bool defaultMode) + { + var options = new JsonSerializerOptions + { + InferClosedTypePolymorphism = true, + }; + + JsonSerializerContext context = defaultMode + ? new PolymorphicTests_Default.PolymorphicTestsContext_Default(options) + : new PolymorphicTestsContext_Metadata(options); + + Assert.Same(options, context.Options); + Assert.Null(options.GetTypeInfo(typeof(ClosedEmptyBase)).PolymorphismOptions); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ClosedTypeInference_FromReferencedAssembly(bool defaultMode) + { + JsonSerializerContext context = defaultMode + ? ClosedInferenceContext_Default.Default + : ClosedInferenceContext_Metadata.Default; + + JsonTypeInfo? typeInfo = context.GetTypeInfo(typeof(ReferencedClosedShape)); + Assert.NotNull(typeInfo); + JsonPolymorphismOptions? polymorphismOptions = typeInfo.PolymorphismOptions; + Assert.NotNull(polymorphismOptions); + + Assert.Collection( + polymorphismOptions.DerivedTypes, + derivedType => + { + Assert.Equal(typeof(ReferencedClosedCircle), derivedType.DerivedType); + Assert.Equal(nameof(ReferencedClosedCircle), derivedType.TypeDiscriminator); + }, + derivedType => + { + Assert.Equal(typeof(ReferencedClosedSquare), derivedType.DerivedType); + Assert.Equal(nameof(ReferencedClosedSquare), derivedType.TypeDiscriminator); + }); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void RuntimeInferClosedTypePolymorphism_WithoutGeneratedMetadata_Throws( + bool defaultMode, + bool closedCollection) + { + var options = new JsonSerializerOptions + { + InferClosedTypePolymorphism = true, + }; + + JsonSerializerContext context = defaultMode + ? new PolymorphicTests_Default.PolymorphicTestsContext_Default(options) + : new PolymorphicTestsContext_Metadata(options); + + Assert.Same(options, context.Options); + + Type baseType = closedCollection ? typeof(ClosedNumberList) : typeof(ClosedShape); + InvalidOperationException exception = + Assert.Throws(() => options.GetTypeInfo(baseType)); + + Assert.Contains(baseType.ToString(), exception.Message); + Assert.Contains( + $"{nameof(JsonSerializerOptions)}.{nameof(JsonSerializerOptions.InferClosedTypePolymorphism)}", + exception.Message); + Assert.Contains( + $"{nameof(JsonSourceGenerationOptionsAttribute)}.{nameof(JsonSourceGenerationOptionsAttribute.InferClosedTypePolymorphism)}", + exception.Message); + } + } + + public sealed partial class PolymorphicTests_Metadata_AsyncStream + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Metadata.Default.Options; + } + + public sealed partial class PolymorphicTests_Default + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Default.Default.Options; + + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedNumberList))] + [JsonSerializable(typeof(ClosedShape))] + internal sealed partial class PolymorphicTestsContext_Default; + } + + public sealed partial class PolymorphicTests_Default_AsyncStream + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Default.Default.Options; + } + + public closed class ClosedNumberList : List; + public sealed class AscendingNumberList : ClosedNumberList; + public sealed class DescendingNumberList : ClosedNumberList; + + // These contexts intentionally include invalid inferred registrations whose diagnostics are + // validated by the corresponding runtime-behavior tests. +#pragma warning disable SYSLIB1229, SYSLIB1240, SYSLIB1241, SYSLIB1242 + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata, InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(ClosedAccessBase))] + [JsonSerializable(typeof(ClosedArrayBase))] + [JsonSerializable(typeof(ClosedCollisionBase))] + // Both inferred types are named Node, so give their generated metadata properties unique names. + [JsonSerializable(typeof(ClosedCollisionHolderA.Node), TypeInfoPropertyName = "ClosedCollisionNodeA")] + [JsonSerializable(typeof(ClosedCollisionHolderB.Node), TypeInfoPropertyName = "ClosedCollisionNodeB")] + [JsonSerializable(typeof(ClosedConcreteMismatchBase))] + [JsonSerializable(typeof(ClosedConstrainedBase>))] + [JsonSerializable(typeof(ClosedConstraintViolationBase))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedContainer>))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedCustomDiscriminatorBase))] + [JsonSerializable(typeof(ClosedDeepJaggedBase>))] + [JsonSerializable(typeof(ClosedDeepJaggedMismatchBase>))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedOne")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntInt")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntString")] + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedExplicitBase))] + [JsonSerializable(typeof(ClosedGroundMismatchBase))] + [JsonSerializable(typeof(ClosedKvpBase>))] + [JsonSerializable(typeof(ClosedMixedBase))] + [JsonSerializable(typeof(ClosedNestedAccessContainer.Base))] + [JsonSerializable(typeof(ClosedNestedArgBase.NestedBox>))] + [JsonSerializable(typeof(ClosedNestedDerivedBase))] + [JsonSerializable(typeof(ClosedNestedMismatchBase.NestedBox>))] + [JsonSerializable(typeof(ClosedPair))] + [JsonSerializable(typeof(ClosedPartialBase))] + [JsonSerializable(typeof(ClosedPayload))] + [JsonSerializable(typeof(ClosedProtectedAccessBase))] + [JsonSerializable(typeof(ClosedRepeatedBase))] + [JsonSerializable(typeof(ClosedRepeatedMismatchBase))] + [JsonSerializable(typeof(ClosedReorderedBase))] + [JsonSerializable(typeof(ClosedShape))] + [JsonSerializable(typeof(ClosedShapeHolder))] + [JsonSerializable(typeof(ClosedTupleBase<(int, string)>))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedWrappedBase>))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(PlainAbstractBase))] + [JsonSerializable(typeof(ReferencedClosedShape))] + internal sealed partial class ClosedInferenceContext_Metadata : JsonSerializerContext; + + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(ClosedAccessBase))] + [JsonSerializable(typeof(ClosedArrayBase))] + [JsonSerializable(typeof(ClosedCollisionBase))] + [JsonSerializable(typeof(ClosedCollisionHolderA.Node), TypeInfoPropertyName = "ClosedCollisionNodeA")] + [JsonSerializable(typeof(ClosedCollisionHolderB.Node), TypeInfoPropertyName = "ClosedCollisionNodeB")] + [JsonSerializable(typeof(ClosedConcreteMismatchBase))] + [JsonSerializable(typeof(ClosedConstrainedBase>))] + [JsonSerializable(typeof(ClosedConstraintViolationBase))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedContainer>))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedCustomDiscriminatorBase))] + [JsonSerializable(typeof(ClosedDeepJaggedBase>))] + [JsonSerializable(typeof(ClosedDeepJaggedMismatchBase>))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedOne")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntInt")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntString")] + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedExplicitBase))] + [JsonSerializable(typeof(ClosedGroundMismatchBase))] + [JsonSerializable(typeof(ClosedKvpBase>))] + [JsonSerializable(typeof(ClosedMixedBase))] + [JsonSerializable(typeof(ClosedNestedAccessContainer.Base))] + [JsonSerializable(typeof(ClosedNestedArgBase.NestedBox>))] + [JsonSerializable(typeof(ClosedNestedDerivedBase))] + [JsonSerializable(typeof(ClosedNestedMismatchBase.NestedBox>))] + [JsonSerializable(typeof(ClosedPair))] + [JsonSerializable(typeof(ClosedPartialBase))] + [JsonSerializable(typeof(ClosedPayload))] + [JsonSerializable(typeof(ClosedProtectedAccessBase))] + [JsonSerializable(typeof(ClosedRepeatedBase))] + [JsonSerializable(typeof(ClosedRepeatedMismatchBase))] + [JsonSerializable(typeof(ClosedReorderedBase))] + [JsonSerializable(typeof(ClosedShape))] + [JsonSerializable(typeof(ClosedShapeHolder))] + [JsonSerializable(typeof(ClosedTupleBase<(int, string)>))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedWrappedBase>))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(PlainAbstractBase))] + [JsonSerializable(typeof(ReferencedClosedShape))] + internal sealed partial class ClosedInferenceContext_Default : JsonSerializerContext; +#pragma warning restore SYSLIB1229, SYSLIB1240, SYSLIB1241, SYSLIB1242 + +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs index 7b5915aa6ac00c..0cf12401e0eeb8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs @@ -251,6 +251,34 @@ public PolymorphicTests_Metadata() [JsonSerializable(typeof(PolymorphicClass.DerivedAbstractClass), TypeInfoPropertyName = "PolymorphicClass_DerivedAbstractClass")] [JsonSerializable(typeof(PolymorphicInterface.DerivedInterface1))] [JsonSerializable(typeof(PolymorphicInterface.DerivedInterface2))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(IOpenGenericBase_Ambiguous>))] + [JsonSerializable(typeof(NestedBase.NestedBox>))] + [JsonSerializable(typeof(NonGenericBaseWithOpenGenericDerived))] + [JsonSerializable(typeof(OpenGenericBase_GroundMismatch))] + [JsonSerializable(typeof(OpenGenericBase_StructConstraint))] + [JsonSerializable(typeof(OpenGenericBase_Unbound))] + [JsonSerializable(typeof(OpenGenericBase_Unresolvable))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericMultiArityBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedOne")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntInt")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntString")] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(PolymorphicDerivedGenericClass))] internal sealed partial class PolymorphicTestsContext_Metadata : JsonSerializerContext { } @@ -497,6 +525,34 @@ public PolymorphicTests_Default() [JsonSerializable(typeof(PolymorphicTests.MyThingCollection))] [JsonSerializable(typeof(PolymorphicTests.MyThingDictionary))] [JsonSerializable(typeof(PolymorphicTests.MyDerivedThing))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(IOpenGenericBase_Ambiguous>))] + [JsonSerializable(typeof(NestedBase.NestedBox>))] + [JsonSerializable(typeof(NonGenericBaseWithOpenGenericDerived))] + [JsonSerializable(typeof(OpenGenericBase_GroundMismatch))] + [JsonSerializable(typeof(OpenGenericBase_StructConstraint))] + [JsonSerializable(typeof(OpenGenericBase_Unbound))] + [JsonSerializable(typeof(OpenGenericBase_Unresolvable))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericMultiArityBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedOne")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntInt")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntString")] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(PolymorphicDerivedGenericClass))] internal sealed partial class PolymorphicTestsContext_Default : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index 66ea58048dcb12..61eadd3fc0e3c5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -26,6 +26,7 @@ + @@ -126,6 +127,7 @@ + @@ -142,6 +144,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs index 6c53ae75f1410d..d86921f0da9eb1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Xunit; @@ -1377,6 +1378,42 @@ public class MyDerived : MyBase> Assert.Empty(result.Diagnostics); } + [Fact] + public void OpenGenericDerivedType_DeepJaggedArgMismatch_WarnsWithSYSLIB1229() + { + string source = """ + using System.Collections.Generic; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSerializable(typeof(Animal>))] + internal partial class JsonContext : JsonSerializerContext + { + } + + [JsonDerivedType(typeof(Silly<>), "silly")] + public class Animal + { + } + + public class Silly : Animal> + { + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Contains("Silly<>", diagnostic.GetMessage()); + Assert.Contains("Animal<", diagnostic.GetMessage()); + Assert.Contains("List", diagnostic.GetMessage()); + } + [Fact] public void OpenGenericDerivedType_ReorderedParameters_CompilesSuccessfully() { @@ -1560,6 +1597,8 @@ public class MyDerived : MyBase where T : struct JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229"); Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("MyDerived<>", diagnostic.GetMessage()); + Assert.Contains("MyBase", diagnostic.GetMessage()); } [Fact] @@ -1991,5 +2030,305 @@ public class ConstraintImpl : ConstraintBase where T : IEnumerable JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation); Assert.Empty(result.Diagnostics); } + + [Fact] + public void ClosedTypeInference_UnresolvableOpenGenericDerivedType_ProducesSYSLIB1229() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(GenericBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class GenericBase { } + public sealed class GenericDerived : GenericBase { } + public sealed class GenericFallback : GenericBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "GenericBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Contains("GenericDerived<>", diagnostic.GetMessage()); + Assert.Contains("GenericBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_ConstraintViolation_ProducesSYSLIB1229() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(GenericBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class GenericBase { } + public sealed class GenericDerived : GenericBase where T : struct { } + public sealed class GenericFallback : GenericBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "GenericBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("GenericDerived<>", diagnostic.GetMessage()); + Assert.Contains("GenericBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_DeepJaggedArgMismatch_ProducesSYSLIB1229() + { + string source = """ + using System.Collections.Generic; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(Animal>))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class Animal { } + public sealed class Silly : Animal> { } + public sealed class Fallback : Animal> { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "Animal"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Contains("Silly<>", diagnostic.GetMessage()); + Assert.Contains("Animal<", diagnostic.GetMessage()); + Assert.Contains("List", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_IncompatibleConstructedDerivedType_ProducesSYSLIB1240() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(ConcreteMismatchBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class ConcreteMismatchBase { } + public sealed class ConcreteMismatchIntDerived : ConcreteMismatchBase { } + public sealed class ConcreteMismatchStringDerived : ConcreteMismatchBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "ConcreteMismatchBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1240", diagnostic.Id); + Assert.Contains("ConcreteMismatchIntDerived", diagnostic.GetMessage()); + Assert.Contains("ConcreteMismatchBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_InaccessibleDerivedType_ProducesSYSLIB1241() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(InaccessibleBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class InaccessibleBase { } + internal sealed class InaccessibleDerived : InaccessibleBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "InaccessibleBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1241", diagnostic.Id); + Assert.Contains("InaccessibleDerived", diagnostic.GetMessage()); + Assert.Contains("InaccessibleBase", diagnostic.GetMessage()); + } + + [Theory] + [InlineData("\"duplicate\"")] + [InlineData("42")] + public void JsonDerivedTypeAttribute_DerivedTypeDiscriminatorCollision_ProducesSYSLIB1242AtAttribute(string discriminator) + { + string source = $$""" + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSerializable(typeof(CollisionBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + [JsonDerivedType(typeof(A), {{discriminator}})] + [JsonDerivedType(typeof(B), {{discriminator}})] + public abstract class CollisionBase { } + public sealed class A : CollisionBase { } + public sealed class B : CollisionBase { } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1242", diagnostic.Id); + Assert.Contains(discriminator.Trim('"'), diagnostic.GetMessage()); + Assert.Contains("CollisionBase", diagnostic.GetMessage()); + + string expectedAttribute = $"JsonDerivedType(typeof(B), {discriminator})"; + Assert.Equal( + expectedAttribute, + diagnostic.Location.SourceTree!.GetText().ToString(diagnostic.Location.SourceSpan)); + } + + [Fact] + public void ClosedTypeInference_DerivedTypeDiscriminatorCollision_ProducesSYSLIB1242() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(CollisionBase))] + [JsonSerializable(typeof(CollisionA.Node), TypeInfoPropertyName = "CollisionANode")] + [JsonSerializable(typeof(CollisionB.Node), TypeInfoPropertyName = "CollisionBNode")] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class CollisionBase { } + public static class CollisionA + { + public sealed class Node : CollisionBase { } + } + public static class CollisionB + { + public sealed class Node : CollisionBase { } + } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "CollisionBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1242", diagnostic.Id); + Assert.Contains("Node", diagnostic.GetMessage()); + Assert.Contains("CollisionBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_GenericDerivedTypesWithSameNameDifferentArities_ProduceSYSLIB1242() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(Animal))] + [JsonSerializable(typeof(Cat), TypeInfoPropertyName = "CatOne")] + [JsonSerializable(typeof(Cat), TypeInfoPropertyName = "CatTwo")] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class Animal { } + public sealed class Cat : Animal { } + public sealed class Cat : Animal { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "Animal"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1242", diagnostic.Id); + Assert.Contains("Cat", diagnostic.GetMessage()); + Assert.Contains("Animal", diagnostic.GetMessage()); + } + + private static Compilation CreateCompilationWithClosedType(string source, string closedTypeName) + { + const string ClosedModifier = "closed"; + const string BinderCompatibleModifier = "partial"; + + string closedDeclaration = $"{ClosedModifier} abstract class {closedTypeName}"; + Assert.Contains(closedDeclaration, source); + + // The unit-test harness uses Roslyn 4.8, which predates the closed modifier. Parse the + // declaration as partial, then restore its text while retaining the binder-recognized + // token kind so the generator's compatibility polyfill observes a closed type. + source = source.Replace( + closedDeclaration, + $"{BinderCompatibleModifier} abstract class {closedTypeName}"); + + Compilation compilation = CompilationHelper.CreateCompilation(source); + SyntaxTree syntaxTree = compilation.SyntaxTrees.First(); + SyntaxNode root = syntaxTree.GetRoot(); + ClassDeclarationSyntax declaration = root + .DescendantNodes() + .OfType() + .Single(declaration => declaration.Identifier.ValueText == closedTypeName); + + SyntaxToken partialModifier = + declaration.Modifiers.Single(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)); + SyntaxToken closedModifier = SyntaxFactory.Token( + partialModifier.LeadingTrivia, + SyntaxKind.PartialKeyword, + ClosedModifier, + ClosedModifier, + partialModifier.TrailingTrivia); + SyntaxNode updatedRoot = root.ReplaceNode( + declaration, + declaration.WithModifiers(declaration.Modifiers.Replace(partialModifier, closedModifier))); + + return compilation.ReplaceSyntaxTree( + syntaxTree, + syntaxTree.WithRootAndOptions(updatedRoot, syntaxTree.Options)); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index ba1ad8d6829210..8aea82575c9cc9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -99,8 +99,8 @@ - + @@ -275,11 +275,6 @@ - - - - - @@ -292,6 +287,7 @@ + From b6155948c8de571e65683c0deff400412bb76aa4 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 16 Jul 2026 11:47:27 +0300 Subject: [PATCH 7/9] Add IsClosedtypeAttribute dynamic dependency --- .../Serialization/JsonSerializerWrapper.Reflection.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs index a45001fe1f592b..93d8dd404757b6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs @@ -21,6 +21,8 @@ public abstract partial class JsonSerializerWrapper // Ensure that the reflection-based serializer testing abstraction roots KeyValuePair<,> // which is required by many tests in the reflection test suite. [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties, typeof(KeyValuePair<,>))] + // Ensure that compiler-generated closed hierarchy metadata is preserved in trimmed reflection tests. + [DynamicDependency(DynamicallyAccessedMemberTypes.All, "System.Runtime.CompilerServices.IsClosedTypeAttribute", "System.Private.CoreLib")] protected JsonSerializerWrapper() { } From aa4de011a99ea510ac98d688ef796d51bec8812a Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 16 Jul 2026 11:57:00 +0300 Subject: [PATCH 8/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/project/list-of-diagnostics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index aa7ddbcfec1739..d8d898c0dbc223 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -288,7 +288,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL | __`SYSLIB1239`__ | _`SYSLIB1230`-`SYSLIB1239` reserved for Microsoft.Interop.ComInterfaceGenerator._ | | __`SYSLIB1240`__ | Derived type is not a supported polymorphic derived type. | | __`SYSLIB1241`__ | Inferred derived type is less accessible than the polymorphic base type. | -| __`SYSLIB1242`__ | Inferred derived types produce a duplicate type discriminator. | +| __`SYSLIB1242`__ | Derived types produce a duplicate type discriminator. | | __`SYSLIB1243`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | | __`SYSLIB1244`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | | __`SYSLIB1245`__ | _`SYSLIB1240`-`SYSLIB1249` reserved for System.Text.Json.SourceGeneration._ | From ed978e91210ac278d5d0be74f5a1c9e1fe6d3331 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 16 Jul 2026 14:05:39 +0300 Subject: [PATCH 9/9] Handle Mono closed hierarchy attribute arrays Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8967afcb-a170-497d-930a-a69e5ccf8773 --- .../DefaultJsonTypeInfoResolver.Helpers.cs | 37 +++++++++++++++---- .../JsonSerializerWrapper.Reflection.cs | 2 - 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index ae94576aa09270..5f34f749cee427 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -209,22 +209,45 @@ private static void ResolveOpenGenericDerivedTypes(Type baseType, IList derivedTypeArguments) + if (namedArgument.MemberName != "DerivedTypes") { - Type[] derivedTypes = new Type[derivedTypeArguments.Count]; - for (int i = 0; i < derivedTypes.Length; i++) + continue; + } + + object? value = namedArgument.TypedValue.Value; + + // Mono materializes array-valued named arguments directly, while CoreCLR wraps + // each element in a CustomAttributeTypedArgument. + if (value is Type[] materializedDerivedTypes) + { + foreach (Type? derivedType in materializedDerivedTypes) { - if (derivedTypeArguments[i].Value is not Type derivedType) + if (derivedType is null) { return null; } + } + + return materializedDerivedTypes; + } - derivedTypes[i] = derivedType; + if (value is not IList derivedTypeArguments) + { + return null; + } + + Type[] derivedTypes = new Type[derivedTypeArguments.Count]; + for (int i = 0; i < derivedTypes.Length; i++) + { + if (derivedTypeArguments[i].Value is not Type derivedType) + { + return null; } - return derivedTypes; + derivedTypes[i] = derivedType; } + + return derivedTypes; } // The closed-type marker is present but carries no derived types. diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs index 93d8dd404757b6..a45001fe1f592b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs @@ -21,8 +21,6 @@ public abstract partial class JsonSerializerWrapper // Ensure that the reflection-based serializer testing abstraction roots KeyValuePair<,> // which is required by many tests in the reflection test suite. [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties, typeof(KeyValuePair<,>))] - // Ensure that compiler-generated closed hierarchy metadata is preserved in trimmed reflection tests. - [DynamicDependency(DynamicallyAccessedMemberTypes.All, "System.Runtime.CompilerServices.IsClosedTypeAttribute", "System.Private.CoreLib")] protected JsonSerializerWrapper() { }