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; public class OpenGenericDerived_GroundMismatch : OpenGenericBase_GroundMismatch; @@ -3202,14 +3236,9 @@ public class OpenGenericDerived_Tuple : OpenGenericBase_Tuple<(T1, T2)> public (T1, T2) Pair { get; set; } } - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_AmbiguousInterfaceMatch_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Impl : IBase, IBase> registered on IBase>. // Both ancestors unify (T=List via the first interface, T=int via the second). // Result: ambiguous, throws. @@ -3217,44 +3246,40 @@ public async Task OpenGenericDerivedType_AmbiguousInterfaceMatch_ThrowsInvalidOp await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericImpl_Ambiguous<>), "impl")] +#pragma warning restore SYSLIB1229 public interface IOpenGenericBase_Ambiguous; public class OpenGenericImpl_Ambiguous : IOpenGenericBase_Ambiguous, IOpenGenericBase_Ambiguous>; - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_UnboundParameter_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Derived : Base — T2 is unspeakable (not bound by the base type's args). var value = new OpenGenericBase_Unbound(); await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericDerived_Unbound<,>), "derived")] +#pragma warning restore SYSLIB1229 public class OpenGenericBase_Unbound; public class OpenGenericDerived_Unbound : OpenGenericBase_Unbound; - [ConditionalFact] + [Fact] public async Task OpenGenericDerivedType_ConstraintViolation_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Derived : Base where T : struct, registered on Base. // Constraint fails → InvalidOperationException. var value = new OpenGenericBase_StructConstraint(); await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(OpenGenericDerived_StructConstraint<>), "derived")] +#pragma warning restore SYSLIB1229 public class OpenGenericBase_StructConstraint; public class OpenGenericDerived_StructConstraint : OpenGenericBase_StructConstraint @@ -3412,6 +3437,274 @@ public class OpenGenericImpl_MultiCtor : OpenGenericImpl_MultiCtor_IntBase, I public T? Item { get; set; } } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GenericSpecialization_ConcreteDerivedTypeForDifferentConstruction_ThrowsInvalidOperationException(bool useStringSpecialization) + { + if (useStringSpecialization) + { + GenericSpecializationAnimal value = new GenericSpecializationDog { Value = "dog", Breed = "collie" }; + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + else + { + GenericSpecializationAnimal value = new GenericSpecializationCat { Value = 42, Lives = 9 }; + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + } + +#pragma warning disable SYSLIB1240 + [JsonDerivedType(typeof(GenericSpecializationCat), "cat")] + [JsonDerivedType(typeof(GenericSpecializationDog), "dog")] +#pragma warning restore SYSLIB1240 + public class GenericSpecializationAnimal + { + public T? Value { get; set; } + } + + public sealed class GenericSpecializationCat : GenericSpecializationAnimal + { + public int Lives { get; set; } + } + + public sealed class GenericSpecializationDog : GenericSpecializationAnimal + { + public string? Breed { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_InterfaceConstraintCompatibleSpecialization_Works() + { + OpenGenericInterfaceConstraintBase> value = + new OpenGenericInterfaceConstraintDerived> { Marker = "valid", Value = [1, 2] }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid","Value":[1,2]}""", json); + + OpenGenericInterfaceConstraintBase> result = + await Serializer.DeserializeWrapper>>(json); + OpenGenericInterfaceConstraintDerived> derived = + Assert.IsType>>(result); + Assert.Equal("valid", derived.Marker); + Assert.Equal([1, 2], derived.Value); + } + + [Theory] + [InlineData(InvalidInterfaceConstraintSpecialization.ListOfString)] + [InlineData(InvalidInterfaceConstraintSpecialization.EnumerableOfString)] + [InlineData(InvalidInterfaceConstraintSpecialization.ValueType)] + public async Task OpenGenericDerivedType_InterfaceConstraintIncompatibleSpecialization_ThrowsInvalidOperationException( + InvalidInterfaceConstraintSpecialization specialization) + { + Func serialize = specialization switch + { + InvalidInterfaceConstraintSpecialization.ListOfString => + () => Serializer.SerializeWrapper(new OpenGenericInterfaceConstraintBase>()), + InvalidInterfaceConstraintSpecialization.EnumerableOfString => + () => Serializer.SerializeWrapper(new OpenGenericInterfaceConstraintBase>()), + InvalidInterfaceConstraintSpecialization.ValueType => + () => Serializer.SerializeWrapper(new OpenGenericInterfaceConstraintBase()), + _ => throw new ArgumentOutOfRangeException(nameof(specialization)), + }; + + await Assert.ThrowsAsync(serialize); + } + + public enum InvalidInterfaceConstraintSpecialization + { + ListOfString, + EnumerableOfString, + ValueType, + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericInterfaceConstraintDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericInterfaceConstraintBase + { + public T? Value { get; set; } + } + + public class OpenGenericInterfaceConstraintDerived : OpenGenericInterfaceConstraintBase + where T : IEnumerable + { + public string? Marker { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_MixedValidAndUnboundRegistrations_ThrowsInvalidOperationException() + { + OpenGenericMultiArityBase value = new OpenGenericMultiArityDerived { Value = 42 }; + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(OpenGenericMultiArityDerived<>), "one")] +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericMultiArityDerived<,>), "two")] +#pragma warning restore SYSLIB1229 + public class OpenGenericMultiArityBase + { + public T? Value { get; set; } + } + + public class OpenGenericMultiArityDerived : OpenGenericMultiArityBase; + + public class OpenGenericMultiArityDerived : OpenGenericMultiArityBase; + + [Fact] + public async Task OpenGenericDerivedType_SameNameDifferentArities_ThrowsOnDiscriminatorCollision() + { + OpenGenericNameCollisionBase value = new OpenGenericNameCollisionDerived(); + InvalidOperationException exception = await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value)); + Assert.Contains(nameof(OpenGenericNameCollisionDerived), exception.Message); + } + + [Fact] + public async Task OpenGenericDerivedType_SameNameDifferentArities_IncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericNameCollisionBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType( + typeof(OpenGenericNameCollisionDerived<>), + nameof(OpenGenericNameCollisionDerived))] +#pragma warning restore SYSLIB1229 +#pragma warning disable SYSLIB1242 // The duplicate discriminator is intentional for this test. + [JsonDerivedType( + typeof(OpenGenericNameCollisionDerived<,>), + nameof(OpenGenericNameCollisionDerived))] +#pragma warning restore SYSLIB1242 + public class OpenGenericNameCollisionBase; + + public class OpenGenericNameCollisionDerived : OpenGenericNameCollisionBase; + + public class OpenGenericNameCollisionDerived : OpenGenericNameCollisionBase; + + [Fact] + public async Task OpenGenericDerivedType_RepeatedTypeParameterCompatibleSpecialization_Works() + { + OpenGenericRepeatedBase value = + new OpenGenericRepeatedDerived { First = 1, Second = 2, Marker = "valid" }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid","First":1,"Second":2}""", json); + + OpenGenericRepeatedBase result = + await Serializer.DeserializeWrapper>(json); + OpenGenericRepeatedDerived derived = Assert.IsType>(result); + Assert.Equal("valid", derived.Marker); + Assert.Equal(1, derived.First); + Assert.Equal(2, derived.Second); + } + + [Fact] + public async Task OpenGenericDerivedType_RepeatedTypeParameterIncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericRepeatedBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericRepeatedDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericRepeatedBase + { + public T1? First { get; set; } + public T2? Second { get; set; } + } + + public class OpenGenericRepeatedDerived : OpenGenericRepeatedBase + { + public string? Marker { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_FixedArgumentCompatibleSpecialization_Works() + { + OpenGenericFixedArgumentBase value = + new OpenGenericFixedArgumentDerived { First = "value", Second = 42, Marker = "valid" }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid","First":"value","Second":42}""", json); + + OpenGenericFixedArgumentBase result = + await Serializer.DeserializeWrapper>(json); + OpenGenericFixedArgumentDerived derived = + Assert.IsType>(result); + Assert.Equal("valid", derived.Marker); + Assert.Equal("value", derived.First); + Assert.Equal(42, derived.Second); + } + + [Fact] + public async Task OpenGenericDerivedType_FixedArgumentIncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericFixedArgumentBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericFixedArgumentDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericFixedArgumentBase + { + public T1? First { get; set; } + public T2? Second { get; set; } + } + + public class OpenGenericFixedArgumentDerived : OpenGenericFixedArgumentBase + { + public string? Marker { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_NewConstraintCompatibleSpecialization_Works() + { + OpenGenericNewConstraintBase value = + new OpenGenericNewConstraintDerived { Marker = "valid" }; + + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Marker":"valid"}""", json); + + OpenGenericNewConstraintBase result = + await Serializer.DeserializeWrapper>(json); + Assert.IsType>(result); + } + + [Fact] + public async Task OpenGenericDerivedType_NewConstraintIncompatibleSpecialization_ThrowsInvalidOperationException() + { + var value = new OpenGenericNewConstraintBase(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + +#pragma warning disable SYSLIB1229 + [JsonDerivedType(typeof(OpenGenericNewConstraintDerived<>), "derived")] +#pragma warning restore SYSLIB1229 + public class OpenGenericNewConstraintBase; + + public class OpenGenericNewConstraintDerived : OpenGenericNewConstraintBase + where T : class, new() + { + public string? Marker { get; set; } + } + + public class NewConstraintArgument; + + public class NoDefaultConstructorConstraintArgument + { + public NoDefaultConstructorConstraintArgument(int value) + { + Value = value; + } + + public int Value { get; } + } + #endregion #region Generic Variance Tests @@ -3527,14 +3820,9 @@ public async Task Variance_BivariantInterface_BothViaVariance_DefaultThrows() // behavior. The reflection side has always handled these cases correctly because // Type.GetGenericArguments() returns enclosing+leaf args together. - [ConditionalFact] + [Fact] public async Task NestedGeneric_EnclosingMismatch_ThrowsInvalidOperationException() { - if (Serializer.IsSourceGeneratedSerializer) - { - throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only."); - } - // Pattern: NestedDerivedEnclosingMismatch : NestedBase.NestedBox>. // Target: NestedBase.NestedBox>. // The enclosing argument differs (int vs string) so unification MUST fail. The @@ -3546,7 +3834,9 @@ public async Task NestedGeneric_EnclosingMismatch_ThrowsInvalidOperationExceptio await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } +#pragma warning disable SYSLIB1229 [JsonDerivedType(typeof(NestedDerivedEnclosingMismatch<>), "nested")] +#pragma warning restore SYSLIB1229 public class NestedBase { public T? Item { get; set; } } public class NestedOuter { public class NestedBox { public TInner? Inner { get; set; } } } public class NestedDerivedEnclosingMismatch : NestedBase.NestedBox>; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs index 4879565121d9ce..aed010244b79ed 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/TestClasses.cs @@ -20,8 +20,42 @@ public class ClassFromOtherAssemblyWithNonPublicMembers internal protected int InternalProtectedValue { get; set; } = 32; } + public closed class ReferencedClosedShape + { + } + + public sealed class ReferencedClosedCircle : ReferencedClosedShape + { + } + + public sealed class ReferencedClosedSquare : ReferencedClosedShape + { + } + [JsonSerializable(typeof(MyPoco))] public partial class NETStandardSerializerContext : JsonSerializerContext { } } + +namespace System.Runtime.CompilerServices +{ + [System.AttributeUsage(System.AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : System.Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + + public bool IsOptional { get; set; } + } + + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + internal sealed class IsClosedTypeAttribute : System.Attribute + { + public System.Type[] DerivedTypes { get; set; } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.ClosedTypeInference.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.ClosedTypeInference.cs new file mode 100644 index 00000000000000..2b894ef92cea74 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.ClosedTypeInference.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Serialization.Tests; +using System.Text.Json.SourceGeneration.Tests.NETStandard; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public sealed partial class PolymorphicTests_Metadata + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Metadata.Default.Options; + + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedNumberList))] + [JsonSerializable(typeof(ClosedShape))] + internal sealed partial class PolymorphicTestsContext_Metadata; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RuntimeInferClosedTypePolymorphism_EmptyHierarchy_IsInert(bool defaultMode) + { + var options = new JsonSerializerOptions + { + InferClosedTypePolymorphism = true, + }; + + JsonSerializerContext context = defaultMode + ? new PolymorphicTests_Default.PolymorphicTestsContext_Default(options) + : new PolymorphicTestsContext_Metadata(options); + + Assert.Same(options, context.Options); + Assert.Null(options.GetTypeInfo(typeof(ClosedEmptyBase)).PolymorphismOptions); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ClosedTypeInference_FromReferencedAssembly(bool defaultMode) + { + JsonSerializerContext context = defaultMode + ? ClosedInferenceContext_Default.Default + : ClosedInferenceContext_Metadata.Default; + + JsonTypeInfo? typeInfo = context.GetTypeInfo(typeof(ReferencedClosedShape)); + Assert.NotNull(typeInfo); + JsonPolymorphismOptions? polymorphismOptions = typeInfo.PolymorphismOptions; + Assert.NotNull(polymorphismOptions); + + Assert.Collection( + polymorphismOptions.DerivedTypes, + derivedType => + { + Assert.Equal(typeof(ReferencedClosedCircle), derivedType.DerivedType); + Assert.Equal(nameof(ReferencedClosedCircle), derivedType.TypeDiscriminator); + }, + derivedType => + { + Assert.Equal(typeof(ReferencedClosedSquare), derivedType.DerivedType); + Assert.Equal(nameof(ReferencedClosedSquare), derivedType.TypeDiscriminator); + }); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void RuntimeInferClosedTypePolymorphism_WithoutGeneratedMetadata_Throws( + bool defaultMode, + bool closedCollection) + { + var options = new JsonSerializerOptions + { + InferClosedTypePolymorphism = true, + }; + + JsonSerializerContext context = defaultMode + ? new PolymorphicTests_Default.PolymorphicTestsContext_Default(options) + : new PolymorphicTestsContext_Metadata(options); + + Assert.Same(options, context.Options); + + Type baseType = closedCollection ? typeof(ClosedNumberList) : typeof(ClosedShape); + InvalidOperationException exception = + Assert.Throws(() => options.GetTypeInfo(baseType)); + + Assert.Contains(baseType.ToString(), exception.Message); + Assert.Contains( + $"{nameof(JsonSerializerOptions)}.{nameof(JsonSerializerOptions.InferClosedTypePolymorphism)}", + exception.Message); + Assert.Contains( + $"{nameof(JsonSourceGenerationOptionsAttribute)}.{nameof(JsonSourceGenerationOptionsAttribute.InferClosedTypePolymorphism)}", + exception.Message); + } + } + + public sealed partial class PolymorphicTests_Metadata_AsyncStream + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Metadata.Default.Options; + } + + public sealed partial class PolymorphicTests_Default + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Default.Default.Options; + + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedNumberList))] + [JsonSerializable(typeof(ClosedShape))] + internal sealed partial class PolymorphicTestsContext_Default; + } + + public sealed partial class PolymorphicTests_Default_AsyncStream + { + protected override JsonSerializerOptions ClosedTypeInferenceOptions => + ClosedInferenceContext_Default.Default.Options; + } + + public closed class ClosedNumberList : List; + public sealed class AscendingNumberList : ClosedNumberList; + public sealed class DescendingNumberList : ClosedNumberList; + + // These contexts intentionally include invalid inferred registrations whose diagnostics are + // validated by the corresponding runtime-behavior tests. +#pragma warning disable SYSLIB1229, SYSLIB1240, SYSLIB1241, SYSLIB1242 + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata, InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(ClosedAccessBase))] + [JsonSerializable(typeof(ClosedArrayBase))] + [JsonSerializable(typeof(ClosedCollisionBase))] + // Both inferred types are named Node, so give their generated metadata properties unique names. + [JsonSerializable(typeof(ClosedCollisionHolderA.Node), TypeInfoPropertyName = "ClosedCollisionNodeA")] + [JsonSerializable(typeof(ClosedCollisionHolderB.Node), TypeInfoPropertyName = "ClosedCollisionNodeB")] + [JsonSerializable(typeof(ClosedConcreteMismatchBase))] + [JsonSerializable(typeof(ClosedConstrainedBase>))] + [JsonSerializable(typeof(ClosedConstraintViolationBase))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedContainer>))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedCustomDiscriminatorBase))] + [JsonSerializable(typeof(ClosedDeepJaggedBase>))] + [JsonSerializable(typeof(ClosedDeepJaggedMismatchBase>))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedOne")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntInt")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntString")] + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedExplicitBase))] + [JsonSerializable(typeof(ClosedGroundMismatchBase))] + [JsonSerializable(typeof(ClosedKvpBase>))] + [JsonSerializable(typeof(ClosedMixedBase))] + [JsonSerializable(typeof(ClosedNestedAccessContainer.Base))] + [JsonSerializable(typeof(ClosedNestedArgBase.NestedBox>))] + [JsonSerializable(typeof(ClosedNestedDerivedBase))] + [JsonSerializable(typeof(ClosedNestedMismatchBase.NestedBox>))] + [JsonSerializable(typeof(ClosedPair))] + [JsonSerializable(typeof(ClosedPartialBase))] + [JsonSerializable(typeof(ClosedPayload))] + [JsonSerializable(typeof(ClosedProtectedAccessBase))] + [JsonSerializable(typeof(ClosedRepeatedBase))] + [JsonSerializable(typeof(ClosedRepeatedMismatchBase))] + [JsonSerializable(typeof(ClosedReorderedBase))] + [JsonSerializable(typeof(ClosedShape))] + [JsonSerializable(typeof(ClosedShapeHolder))] + [JsonSerializable(typeof(ClosedTupleBase<(int, string)>))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedWrappedBase>))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(PlainAbstractBase))] + [JsonSerializable(typeof(ReferencedClosedShape))] + internal sealed partial class ClosedInferenceContext_Metadata : JsonSerializerContext; + + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(ClosedAccessBase))] + [JsonSerializable(typeof(ClosedArrayBase))] + [JsonSerializable(typeof(ClosedCollisionBase))] + [JsonSerializable(typeof(ClosedCollisionHolderA.Node), TypeInfoPropertyName = "ClosedCollisionNodeA")] + [JsonSerializable(typeof(ClosedCollisionHolderB.Node), TypeInfoPropertyName = "ClosedCollisionNodeB")] + [JsonSerializable(typeof(ClosedConcreteMismatchBase))] + [JsonSerializable(typeof(ClosedConstrainedBase>))] + [JsonSerializable(typeof(ClosedConstraintViolationBase))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedContainer>))] + [JsonSerializable(typeof(ClosedContainer))] + [JsonSerializable(typeof(ClosedCustomDiscriminatorBase))] + [JsonSerializable(typeof(ClosedDeepJaggedBase>))] + [JsonSerializable(typeof(ClosedDeepJaggedMismatchBase>))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityBase))] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedOne")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntInt")] + [JsonSerializable(typeof(ClosedDuplicateArityDerived), TypeInfoPropertyName = "ClosedDuplicateArityDerivedTwoIntString")] + [JsonSerializable(typeof(ClosedEmptyBase))] + [JsonSerializable(typeof(ClosedExplicitBase))] + [JsonSerializable(typeof(ClosedGroundMismatchBase))] + [JsonSerializable(typeof(ClosedKvpBase>))] + [JsonSerializable(typeof(ClosedMixedBase))] + [JsonSerializable(typeof(ClosedNestedAccessContainer.Base))] + [JsonSerializable(typeof(ClosedNestedArgBase.NestedBox>))] + [JsonSerializable(typeof(ClosedNestedDerivedBase))] + [JsonSerializable(typeof(ClosedNestedMismatchBase.NestedBox>))] + [JsonSerializable(typeof(ClosedPair))] + [JsonSerializable(typeof(ClosedPartialBase))] + [JsonSerializable(typeof(ClosedPayload))] + [JsonSerializable(typeof(ClosedProtectedAccessBase))] + [JsonSerializable(typeof(ClosedRepeatedBase))] + [JsonSerializable(typeof(ClosedRepeatedMismatchBase))] + [JsonSerializable(typeof(ClosedReorderedBase))] + [JsonSerializable(typeof(ClosedShape))] + [JsonSerializable(typeof(ClosedShapeHolder))] + [JsonSerializable(typeof(ClosedTupleBase<(int, string)>))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedUnspeakableBase))] + [JsonSerializable(typeof(ClosedWrappedBase>))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(PlainAbstractBase))] + [JsonSerializable(typeof(ReferencedClosedShape))] + internal sealed partial class ClosedInferenceContext_Default : JsonSerializerContext; +#pragma warning restore SYSLIB1229, SYSLIB1240, SYSLIB1241, SYSLIB1242 + +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs index 7b5915aa6ac00c..0cf12401e0eeb8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphicTests.cs @@ -251,6 +251,34 @@ public PolymorphicTests_Metadata() [JsonSerializable(typeof(PolymorphicClass.DerivedAbstractClass), TypeInfoPropertyName = "PolymorphicClass_DerivedAbstractClass")] [JsonSerializable(typeof(PolymorphicInterface.DerivedInterface1))] [JsonSerializable(typeof(PolymorphicInterface.DerivedInterface2))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(IOpenGenericBase_Ambiguous>))] + [JsonSerializable(typeof(NestedBase.NestedBox>))] + [JsonSerializable(typeof(NonGenericBaseWithOpenGenericDerived))] + [JsonSerializable(typeof(OpenGenericBase_GroundMismatch))] + [JsonSerializable(typeof(OpenGenericBase_StructConstraint))] + [JsonSerializable(typeof(OpenGenericBase_Unbound))] + [JsonSerializable(typeof(OpenGenericBase_Unresolvable))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericMultiArityBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedOne")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntInt")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntString")] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(PolymorphicDerivedGenericClass))] internal sealed partial class PolymorphicTestsContext_Metadata : JsonSerializerContext { } @@ -497,6 +525,34 @@ public PolymorphicTests_Default() [JsonSerializable(typeof(PolymorphicTests.MyThingCollection))] [JsonSerializable(typeof(PolymorphicTests.MyThingDictionary))] [JsonSerializable(typeof(PolymorphicTests.MyDerivedThing))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(GenericSpecializationAnimal))] + [JsonSerializable(typeof(IOpenGenericBase_Ambiguous>))] + [JsonSerializable(typeof(NestedBase.NestedBox>))] + [JsonSerializable(typeof(NonGenericBaseWithOpenGenericDerived))] + [JsonSerializable(typeof(OpenGenericBase_GroundMismatch))] + [JsonSerializable(typeof(OpenGenericBase_StructConstraint))] + [JsonSerializable(typeof(OpenGenericBase_Unbound))] + [JsonSerializable(typeof(OpenGenericBase_Unresolvable))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericDeepJaggedBase>))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericFixedArgumentBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericInterfaceConstraintBase>))] + [JsonSerializable(typeof(OpenGenericMultiArityBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionBase))] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedOne")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntInt")] + [JsonSerializable(typeof(OpenGenericNameCollisionDerived), TypeInfoPropertyName = "OpenGenericNameCollisionDerivedTwoIntString")] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericNewConstraintBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(OpenGenericRepeatedBase))] + [JsonSerializable(typeof(PolymorphicDerivedGenericClass))] internal sealed partial class PolymorphicTestsContext_Default : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index 66ea58048dcb12..61eadd3fc0e3c5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -26,6 +26,7 @@ + @@ -126,6 +127,7 @@ + @@ -142,6 +144,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs index 6c53ae75f1410d..d86921f0da9eb1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Xunit; @@ -1377,6 +1378,42 @@ public class MyDerived : MyBase> Assert.Empty(result.Diagnostics); } + [Fact] + public void OpenGenericDerivedType_DeepJaggedArgMismatch_WarnsWithSYSLIB1229() + { + string source = """ + using System.Collections.Generic; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSerializable(typeof(Animal>))] + internal partial class JsonContext : JsonSerializerContext + { + } + + [JsonDerivedType(typeof(Silly<>), "silly")] + public class Animal + { + } + + public class Silly : Animal> + { + } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Contains("Silly<>", diagnostic.GetMessage()); + Assert.Contains("Animal<", diagnostic.GetMessage()); + Assert.Contains("List", diagnostic.GetMessage()); + } + [Fact] public void OpenGenericDerivedType_ReorderedParameters_CompilesSuccessfully() { @@ -1560,6 +1597,8 @@ public class MyDerived : MyBase where T : struct JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229"); Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("MyDerived<>", diagnostic.GetMessage()); + Assert.Contains("MyBase", diagnostic.GetMessage()); } [Fact] @@ -1991,5 +2030,305 @@ public class ConstraintImpl : ConstraintBase where T : IEnumerable JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation); Assert.Empty(result.Diagnostics); } + + [Fact] + public void ClosedTypeInference_UnresolvableOpenGenericDerivedType_ProducesSYSLIB1229() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(GenericBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class GenericBase { } + public sealed class GenericDerived : GenericBase { } + public sealed class GenericFallback : GenericBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "GenericBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Contains("GenericDerived<>", diagnostic.GetMessage()); + Assert.Contains("GenericBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_ConstraintViolation_ProducesSYSLIB1229() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(GenericBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class GenericBase { } + public sealed class GenericDerived : GenericBase where T : struct { } + public sealed class GenericFallback : GenericBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "GenericBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("GenericDerived<>", diagnostic.GetMessage()); + Assert.Contains("GenericBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_DeepJaggedArgMismatch_ProducesSYSLIB1229() + { + string source = """ + using System.Collections.Generic; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(Animal>))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class Animal { } + public sealed class Silly : Animal> { } + public sealed class Fallback : Animal> { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "Animal"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1229", diagnostic.Id); + Assert.Contains("Silly<>", diagnostic.GetMessage()); + Assert.Contains("Animal<", diagnostic.GetMessage()); + Assert.Contains("List", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_IncompatibleConstructedDerivedType_ProducesSYSLIB1240() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(ConcreteMismatchBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class ConcreteMismatchBase { } + public sealed class ConcreteMismatchIntDerived : ConcreteMismatchBase { } + public sealed class ConcreteMismatchStringDerived : ConcreteMismatchBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "ConcreteMismatchBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1240", diagnostic.Id); + Assert.Contains("ConcreteMismatchIntDerived", diagnostic.GetMessage()); + Assert.Contains("ConcreteMismatchBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_InaccessibleDerivedType_ProducesSYSLIB1241() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(InaccessibleBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class InaccessibleBase { } + internal sealed class InaccessibleDerived : InaccessibleBase { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "InaccessibleBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1241", diagnostic.Id); + Assert.Contains("InaccessibleDerived", diagnostic.GetMessage()); + Assert.Contains("InaccessibleBase", diagnostic.GetMessage()); + } + + [Theory] + [InlineData("\"duplicate\"")] + [InlineData("42")] + public void JsonDerivedTypeAttribute_DerivedTypeDiscriminatorCollision_ProducesSYSLIB1242AtAttribute(string discriminator) + { + string source = $$""" + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSerializable(typeof(CollisionBase))] + internal partial class JsonContext : JsonSerializerContext + { + } + + [JsonDerivedType(typeof(A), {{discriminator}})] + [JsonDerivedType(typeof(B), {{discriminator}})] + public abstract class CollisionBase { } + public sealed class A : CollisionBase { } + public sealed class B : CollisionBase { } + } + """; + + Compilation compilation = CompilationHelper.CreateCompilation(source); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1242", diagnostic.Id); + Assert.Contains(discriminator.Trim('"'), diagnostic.GetMessage()); + Assert.Contains("CollisionBase", diagnostic.GetMessage()); + + string expectedAttribute = $"JsonDerivedType(typeof(B), {discriminator})"; + Assert.Equal( + expectedAttribute, + diagnostic.Location.SourceTree!.GetText().ToString(diagnostic.Location.SourceSpan)); + } + + [Fact] + public void ClosedTypeInference_DerivedTypeDiscriminatorCollision_ProducesSYSLIB1242() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(CollisionBase))] + [JsonSerializable(typeof(CollisionA.Node), TypeInfoPropertyName = "CollisionANode")] + [JsonSerializable(typeof(CollisionB.Node), TypeInfoPropertyName = "CollisionBNode")] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class CollisionBase { } + public static class CollisionA + { + public sealed class Node : CollisionBase { } + } + public static class CollisionB + { + public sealed class Node : CollisionBase { } + } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "CollisionBase"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1242", diagnostic.Id); + Assert.Contains("Node", diagnostic.GetMessage()); + Assert.Contains("CollisionBase", diagnostic.GetMessage()); + } + + [Fact] + public void ClosedTypeInference_GenericDerivedTypesWithSameNameDifferentArities_ProduceSYSLIB1242() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] + [JsonSerializable(typeof(Animal))] + [JsonSerializable(typeof(Cat), TypeInfoPropertyName = "CatOne")] + [JsonSerializable(typeof(Cat), TypeInfoPropertyName = "CatTwo")] + internal partial class JsonContext : JsonSerializerContext + { + } + + public closed abstract class Animal { } + public sealed class Cat : Animal { } + public sealed class Cat : Animal { } + } + """; + + Compilation compilation = CreateCompilationWithClosedType(source, "Animal"); + JsonSourceGeneratorResult result = + CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true); + + Diagnostic diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("SYSLIB1242", diagnostic.Id); + Assert.Contains("Cat", diagnostic.GetMessage()); + Assert.Contains("Animal", diagnostic.GetMessage()); + } + + private static Compilation CreateCompilationWithClosedType(string source, string closedTypeName) + { + const string ClosedModifier = "closed"; + const string BinderCompatibleModifier = "partial"; + + string closedDeclaration = $"{ClosedModifier} abstract class {closedTypeName}"; + Assert.Contains(closedDeclaration, source); + + // The unit-test harness uses Roslyn 4.8, which predates the closed modifier. Parse the + // declaration as partial, then restore its text while retaining the binder-recognized + // token kind so the generator's compatibility polyfill observes a closed type. + source = source.Replace( + closedDeclaration, + $"{BinderCompatibleModifier} abstract class {closedTypeName}"); + + Compilation compilation = CompilationHelper.CreateCompilation(source); + SyntaxTree syntaxTree = compilation.SyntaxTrees.First(); + SyntaxNode root = syntaxTree.GetRoot(); + ClassDeclarationSyntax declaration = root + .DescendantNodes() + .OfType() + .Single(declaration => declaration.Identifier.ValueText == closedTypeName); + + SyntaxToken partialModifier = + declaration.Modifiers.Single(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)); + SyntaxToken closedModifier = SyntaxFactory.Token( + partialModifier.LeadingTrivia, + SyntaxKind.PartialKeyword, + ClosedModifier, + ClosedModifier, + partialModifier.TrailingTrivia); + SyntaxNode updatedRoot = root.ReplaceNode( + declaration, + declaration.WithModifiers(declaration.Modifiers.Replace(partialModifier, closedModifier))); + + return compilation.ReplaceSyntaxTree( + syntaxTree, + syntaxTree.WithRootAndOptions(updatedRoot, syntaxTree.Options)); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs index d8c6b1c4ecbb4a..e6d16145603d0b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs @@ -384,6 +384,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.ReferenceHandler)), ReferenceHandler.Preserve); yield return (GetProp(nameof(JsonSerializerOptions.TypeInfoResolver)), new DefaultJsonTypeInfoResolver()); yield return (GetProp(nameof(JsonSerializerOptions.AllowDuplicateProperties)), false /* true is default */); + yield return (GetProp(nameof(JsonSerializerOptions.InferClosedTypePolymorphism)), true); static PropertyInfo GetProp(string name) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index e152d9e04a93be..8aea82575c9cc9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -99,6 +99,7 @@ + @@ -286,6 +287,7 @@ +