diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md
index 9f312d97c5aa18..d8d898c0dbc223 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`__ | 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/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 5be4c1ff76d97f..09b92d896b404d 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 DerivedTypeDiscriminatorCollision { get; } = DiagnosticDescriptorHelper.Create(
+ id: "SYSLIB1242",
+ 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 7b50edd6bc6857..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)
@@ -686,20 +690,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 +738,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 +791,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,");
@@ -1959,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;
@@ -2119,6 +2110,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..62442ea5f027c9 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
@@ -433,6 +433,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
char? indentCharacter = null;
int? indentSize = null;
bool? allowDuplicateProperties = null;
+ bool? inferClosedTypePolymorphism = null;
if (attributeData.ConstructorArguments.Length > 0)
{
@@ -562,6 +563,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 +620,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
IndentCharacter = indentCharacter,
IndentSize = indentSize,
AllowDuplicateProperties = allowDuplicateProperties,
+ InferClosedTypePolymorphism = inferClosedTypePolymorphism,
};
}
@@ -697,7 +703,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,
@@ -707,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;
@@ -907,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,
@@ -943,6 +954,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,
@@ -952,6 +964,7 @@ private void ProcessTypeCustomAttributes(
out bool foundJsonConverterAttribute,
out TypeRef? customConverterType,
out PolymorphismOptionsSpec? polymorphismOptions,
+ out bool hasClosedDerivedTypes,
out TypeRef? unionClassifierFactoryType)
{
numberHandling = null;
@@ -962,6 +975,7 @@ private void ProcessTypeCustomAttributes(
customConverterType = null;
foundJsonConverterAttribute = false;
polymorphismOptions = null;
+ hasClosedDerivedTypes = false;
unionClassifierFactoryType = null;
bool hasPolymorphicAttribute = false;
@@ -970,6 +984,8 @@ private void ProcessTypeCustomAttributes(
string? typeDiscriminatorPropertyName = null;
TypeRef? polymorphicClassifierFactoryType = null;
List? derivedTypes = null;
+ HashSet? typeDiscriminators = null;
+ bool hasExplicitDerivedTypeAttribute = false;
bool hasUnionTypeClassifierSpecified = false;
bool isUnionType = IsUnionType(typeToGenerate.Type);
INamedTypeSymbol? namedUnionType = typeToGenerate.Type as INamedTypeSymbol;
@@ -1039,25 +1055,8 @@ private void ProcessTypeCustomAttributes(
if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonDerivedTypeAttributeType))
{
Debug.Assert(attributeData.ConstructorArguments.Length > 0);
- 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))
- {
- ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, attributeData.GetLocation(), derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason);
- continue;
- }
-
- derivedType = resolvedType!;
- }
-
- TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode);
-
- // The generated polymorphic metadata references the derived type by name.
- AddExperimentalDiagnosticIds(derivedType, ref experimentalIds);
+ hasExplicitDerivedTypeAttribute = true;
+ ITypeSymbol derivedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!;
object? typeDiscriminator = null;
if (attributeData.ConstructorArguments.Length == 2)
@@ -1066,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))
{
@@ -1120,6 +1119,30 @@ namedUnionType is not null &&
}
}
+ // 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())
+ {
+ 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 })
{
polymorphismOptions = new PolymorphismOptionsSpec
@@ -1140,6 +1163,116 @@ namedUnionType is not null &&
}
}
+ private void AddPolymorphicDerivedType(
+ in TypeToGenerate typeToGenerate,
+ ITypeSymbol derivedType,
+ object? typeDiscriminator,
+ Location? derivedTypeDiagnosticLocation,
+ Location? polymorphismDiagnosticLocation,
+ ref HashSet? typeDiscriminators,
+ ref HashSet? experimentalIds,
+ ref List? derivedTypes,
+ bool isInferredDerivedType)
+ {
+ ITypeSymbol? resolvedDerivedType = derivedType;
+
+ if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived)
+ {
+ if (!TryResolveOpenGenericDerivedType(
+ unboundDerived, typeToGenerate.Type,
+ out resolvedDerivedType, 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 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);
+ }
+ }
+
+ 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());
+ }
+
+ derivedType = resolvedDerivedType ?? _knownSymbols.ObjectType;
+
+ // 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 (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());
+ }
+
+ if (derivedTypes is null && typeToGenerate.Mode == JsonSourceGenerationMode.Serialization)
+ {
+ ReportDiagnostic(DiagnosticDescriptors.PolymorphismNotSupported, polymorphismDiagnosticLocation, typeToGenerate.Type.ToDisplayString());
+ }
+
+ TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode);
+
+ // The generated polymorphic metadata references the derived type by name.
+ AddExperimentalDiagnosticIds(derivedType, ref experimentalIds);
+
+ (derivedTypes ??= new()).Add(new DerivedTypeSpec
+ {
+ DerivedType = derivedTypeRef,
+ TypeDiscriminator = typeDiscriminator,
+ });
+ }
+
+ ///
+ /// 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 void InferClosedTypeDerivedTypes(
+ in TypeToGenerate typeToGenerate,
+ List closedTypeDerivedTypes,
+ ref HashSet? typeDiscriminators,
+ ref HashSet? experimentalIds,
+ ref List? derivedTypes)
+ {
+ // 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);
+ }
+ }
+
///
/// Source-gen-side resolver: closes against
/// via structural unification at compile time.
@@ -1159,7 +1292,7 @@ namedUnionType is not null &&
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/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/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 446fe72efc6ca2..35494412884c9e 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.
+
+
+ Derived types produce a duplicate type discriminator.
+
+
+ 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 b55f6d6983d368..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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..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
@@ -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 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.
+
+
+
+ Derived types produce a duplicate type discriminator.
+ 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/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.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..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
@@ -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,30 @@ 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. The simple name of each derived type,
+ /// equivalent to the result of nameof , is used as its type discriminator.
+ ///
+ 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..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
@@ -87,6 +87,32 @@ internal static void PopulatePolymorphismMetadata(JsonTypeInfo typeInfo)
JsonPolymorphismOptions? options = JsonPolymorphismOptions.CreateFromAttributeDeclarations(typeInfo.Type, out JsonPolymorphicAttribute? polymorphicAttribute);
+ // '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) &&
+ GetInferredClosedDerivedTypes(typeInfo.Type) is { Length: > 0 } inferredDerivedTypes)
+ {
+ options ??= new();
+
+ foreach (Type derivedType in inferredDerivedTypes)
+ {
+ // 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, GetInferredTypeDiscriminator(derivedType)));
+ }
+ }
+
if (options is not null)
{
ResolveOpenGenericDerivedTypes(typeInfo.Type, options.DerivedTypes);
@@ -111,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)]
@@ -156,6 +189,74 @@ private static void ResolveOpenGenericDerivedTypes(Type baseType, IList
+ /// 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 Type[]? GetInferredClosedDerivedTypes(Type type)
+ {
+ foreach (CustomAttributeData attributeData in type.GetCustomAttributesData())
+ {
+ Type attributeType = attributeData.AttributeType;
+ if (attributeType.Name != "IsClosedTypeAttribute" ||
+ attributeType.FullName != "System.Runtime.CompilerServices.IsClosedTypeAttribute")
+ {
+ continue;
+ }
+
+ foreach (CustomAttributeNamedArgument namedArgument in attributeData.NamedArguments)
+ {
+ if (namedArgument.MemberName != "DerivedTypes")
+ {
+ 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 (derivedType is null)
+ {
+ return null;
+ }
+ }
+
+ return materializedDerivedTypes;
+ }
+
+ 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;
+ }
+
+ derivedTypes[i] = derivedType;
+ }
+
+ return derivedTypes;
+ }
+
+ // The closed-type marker is present but carries no derived types.
+ return null;
+ }
+
+ return null;
+ }
+
///
/// Reflection-side resolver: closes against the
/// constructed base type identified by ( ,
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/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..54e643fd411301
--- /dev/null
+++ b/src/libraries/System.Text.Json/tests/Common/PolymorphicTests.ClosedTypeInference.cs
@@ -0,0 +1,830 @@
+// 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.Linq;
+using System.Text.Json.Serialization.Metadata;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public abstract partial class PolymorphicTests
+ {
+ protected virtual JsonSerializerOptions ClosedTypeInferenceOptions =>
+ field ??= new(Serializer.DefaultOptions)
+ {
+ InferClosedTypePolymorphism = true,
+ };
+
+ 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()
+ {
+ 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[]
+ {
+ new ClosedTriangle { Name = "triangle", BaseLength = 5, Height = 6 },
+ """{"$type":"ClosedTriangle","BaseLength":5,"Height":6,"Name":"triangle"}""",
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(BasicClosedHierarchyData))]
+ public async Task ClosedTypeInference_BasicHierarchy_EmitsAndReadsTypeDiscriminator(
+ ClosedShape value,
+ string expectedJson)
+ {
+ JsonSerializerOptions options = ClosedTypeInferenceOptions;
+ Type expectedDerivedType = value.GetType();
+
+ string json = await Serializer.SerializeWrapper(value, options);
+ JsonTestHelper.AssertJsonEqual(expectedJson, json);
+
+ ClosedShape roundtripped = await Serializer.DeserializeWrapper(json, options);
+ Assert.IsType(expectedDerivedType, roundtripped);
+
+ string roundtrippedJson = await Serializer.SerializeWrapper(roundtripped, options);
+ JsonTestHelper.AssertJsonEqual(expectedJson, roundtrippedJson);
+ }
+
+ [Fact]
+ public void ClosedTypeInference_InferredDiscriminatorsMatchSimpleTypeName()
+ {
+ Assert.Equal(
+ new[] { "ClosedCircle", "ClosedSquare", "ClosedTriangle" },
+ GetInferredDiscriminators(ClosedTypeInferenceOptions, typeof(ClosedShape)));
+ Assert.Equal(
+ new[] { nameof(ClosedBag), nameof(ClosedBox) },
+ GetInferredDiscriminators(ClosedTypeInferenceOptions, typeof(ClosedContainer)));
+ }
+
+ [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);
+ }
+
+ [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","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 =>
+ {
+ 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()
+ {
+ JsonSerializerOptions options = ClosedTypeInferenceOptions;
+
+ 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","SideLength":4,"Name":"nested-square"}}""",
+ json);
+
+ ClosedShapeHolder roundtripped = await Serializer.DeserializeWrapper(json, options);
+ Assert.Equal("holder", roundtripped.Name);
+ ClosedSquare square = Assert.IsType(roundtripped.Shape);
+ Assert.Equal("nested-square", square.Name);
+ Assert.Equal(4, square.SideLength);
+ }
+
+ [Fact]
+ public async Task ClosedTypeInference_DeserializeUnknownDiscriminator_Throws()
+ {
+ await Assert.ThrowsAsync(
+ () => Serializer.DeserializeWrapper(
+ """{"$type":"Nonexistent"}""",
+ ClosedTypeInferenceOptions));
+ }
+
+ [Fact]
+ public async Task ClosedTypeInference_FlagDisabled_DoesNotInferPolymorphism()
+ {
+ JsonSerializerOptions options = Serializer.DefaultOptions;
+ ClosedShape value = new ClosedCircle { Name = "circle", Radius = 3 };
+ string json = await Serializer.SerializeWrapper(value, options);
+
+ JsonTestHelper.AssertJsonEqual("""{"Name":"circle"}""", json);
+ Assert.Null(options.GetTypeInfo(typeof(ClosedShape)).PolymorphismOptions);
+ }
+
+ [Fact]
+ public void ClosedTypeInference_EmptyDerivedTypes_IsInert()
+ {
+ Assert.Null(ClosedTypeInferenceOptions.GetTypeInfo(typeof(ClosedEmptyBase)).PolymorphismOptions);
+ }
+
+ [Fact]
+ public async Task ClosedTypeInference_PlainAbstractClass_IsNotInferred()
+ {
+ JsonSerializerOptions options = ClosedTypeInferenceOptions;
+ Assert.Null(options.GetTypeInfo(typeof(PlainAbstractBase)).PolymorphismOptions);
+
+ PlainAbstractBase value = new PlainAbstractDerived();
+ string json = await Serializer.SerializeWrapper(value, options);
+ Assert.DoesNotContain("$type", json);
+ }
+
+ public static IEnumerable GenericClosedHierarchyData()
+ {
+ 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[]
+ {
+ 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);
+
+ object roundtripped = await Serializer.DeserializeWrapper(json, baseType, options);
+ Assert.IsType(expectedDerivedType, roundtripped);
+
+ 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));
+ }
+
+ [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()
+ {
+ 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[]
+ {
+ 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));
+ }
+
+ [Fact]
+ public async Task ClosedTypeInference_ExplicitJsonDerivedType_SuppressesInference()
+ {
+ JsonSerializerOptions options = ClosedTypeInferenceOptions;
+ Assert.Equal(
+ new[] { "customA", "customB" },
+ GetInferredDiscriminators(options, typeof(ClosedExplicitBase)));
+
+ 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_JsonPolymorphicAttribute_HonorsCustomDiscriminatorName()
+ {
+ ClosedCustomDiscriminatorBase value = new ClosedCustomDiscriminatorDerived
+ {
+ 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; }
+ }
+
+ 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; }
+ }
+
+ 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; }
+ }
+ }
+
+ 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; }
+ }
+}
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..68ed6cf3a5fb39 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]
@@ -2631,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 { }
@@ -2657,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
@@ -2813,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()
{
@@ -2848,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; }
@@ -2887,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
@@ -2905,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