diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index 306e9f9d8f2e83..6db6e267c4d8cf 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -272,7 +272,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL | __`SYSLIB1224`__ | Types annotated with JsonSerializableAttribute must be classes deriving from JsonSerializerContext. | | __`SYSLIB1225`__ | Type includes ref like property, field or constructor parameter. | | __`SYSLIB1226`__ | 'JsonIgnoreCondition.Always' is not valid on type-level 'JsonIgnoreAttribute' annotations. | -| __`SYSLIB1227`__ | _`SYSLIB1220`-`SYSLIB1229` reserved for System.Text.Json.SourceGeneration._ | +| __`SYSLIB1227`__ | Open generic derived type is not resolvable for the polymorphic base type. | | __`SYSLIB1228`__ | _`SYSLIB1220`-`SYSLIB1229` reserved for System.Text.Json.SourceGeneration._ | | __`SYSLIB1229`__ | _`SYSLIB1220`-`SYSLIB1229` reserved for System.Text.Json.SourceGeneration._ | | __`SYSLIB1230`__ | Deriving from a `GeneratedComInterface`-attributed interface defined in another assembly is not supported. | diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs index a926b75a5c1631..168c5d311019cf 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs @@ -131,6 +131,14 @@ internal static class DiagnosticDescriptors category: JsonConstants.SystemTextJsonSourceGenerationName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static DiagnosticDescriptor OpenGenericDerivedTypeNotSupported { get; } = DiagnosticDescriptorHelper.Create( + id: "SYSLIB1227", + title: new LocalizableResourceString(nameof(SR.OpenGenericDerivedTypeNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.OpenGenericDerivedTypeNotSupportedMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: JsonConstants.SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index d30851a469dc1b..a09267839f25f4 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -28,6 +28,7 @@ private sealed partial class Emitter private const string NumberHandlingPropName = "NumberHandling"; private const string UnmappedMemberHandlingPropName = "UnmappedMemberHandling"; private const string PreferredPropertyObjectCreationHandlingPropName = "PreferredPropertyObjectCreationHandling"; + private const string PolymorphismOptionsPropName = "PolymorphismOptions"; private const string ObjectCreatorPropName = "ObjectCreator"; private const string OptionsInstanceVariableName = "Options"; private const string JsonTypeInfoLocalVariableName = "jsonTypeInfo"; @@ -75,6 +76,8 @@ private sealed partial class Emitter private const string JsonParameterInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues"; private const string JsonPropertyInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo"; private const string JsonPropertyInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues"; + private const string JsonDerivedTypeTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonDerivedType"; + private const string JsonPolymorphismOptionsTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPolymorphismOptions"; private const string JsonTypeInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonTypeInfo"; private const string JsonTypeInfoResolverTypeRef = "global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver"; private const string ReferenceHandlerTypeRef = "global::System.Text.Json.Serialization.ReferenceHandler"; @@ -582,6 +585,26 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene } } + if (typeMetadata.OpenGenericDerivedTypes is { Count: > 0 } openGenericDerivedTypes) + { + const string OptionsVarName = "polymorphismOptions"; + writer.WriteLine(); + writer.WriteLine($"var {OptionsVarName} = new {JsonPolymorphismOptionsTypeRef}();"); + + foreach (PolymorphicDerivedTypeSpec derivedTypeSpec in openGenericDerivedTypes) + { + string discriminatorArg = derivedTypeSpec.TypeDiscriminator switch + { + string s => $", \"{s}\"", + int i => $", {i}", + _ => "", + }; + writer.WriteLine($"{OptionsVarName}.DerivedTypes.Add(new {JsonDerivedTypeTypeRef}(typeof({derivedTypeSpec.DerivedType.FullyQualifiedName}){discriminatorArg}));"); + } + + writer.WriteLine($"{JsonTypeInfoLocalVariableName}.{PolymorphismOptionsPropName} = {OptionsVarName};"); + } + GenerateTypeInfoFactoryFooter(writer); if (propInitMethodName != null) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 5428245c7ce1b1..7efebea9490f5b 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -601,7 +601,8 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener out JsonIgnoreCondition? typeIgnoreCondition, out bool foundJsonConverterAttribute, out TypeRef? customConverterType, - out bool isPolymorphic); + out bool isPolymorphic, + out List? openGenericDerivedTypes); if (type is { IsRefLikeType: true } or INamedTypeSymbol { IsUnboundGenericType: true } or IErrorTypeSymbol) { @@ -732,6 +733,7 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener ClassType = classType, PrimitiveTypeKind = primitiveTypeKind, IsPolymorphic = isPolymorphic, + OpenGenericDerivedTypes = openGenericDerivedTypes?.OrderBy(d => d.DerivedType.FullyQualifiedName).ToImmutableEquatableArray(), NumberHandling = numberHandling, UnmappedMemberHandling = unmappedMemberHandling, PreferredPropertyObjectCreationHandling = preferredPropertyObjectCreationHandling, @@ -770,7 +772,8 @@ private void ProcessTypeCustomAttributes( out JsonIgnoreCondition? typeIgnoreCondition, out bool foundJsonConverterAttribute, out TypeRef? customConverterType, - out bool isPolymorphic) + out bool isPolymorphic, + out List? openGenericDerivedTypes) { numberHandling = null; unmappedMemberHandling = null; @@ -780,6 +783,7 @@ private void ProcessTypeCustomAttributes( customConverterType = null; foundJsonConverterAttribute = false; isPolymorphic = false; + openGenericDerivedTypes = null; foreach (AttributeData attributeData in typeToGenerate.Type.GetAttributes()) { @@ -847,7 +851,32 @@ private void ProcessTypeCustomAttributes( { Debug.Assert(attributeData.ConstructorArguments.Length > 0); var derivedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!; - EnqueueType(derivedType, typeToGenerate.Mode); + + if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived) + { + if (TryResolveOpenGenericDerivedType(unboundDerived, typeToGenerate.Type, out INamedTypeSymbol? resolvedType)) + { + EnqueueType(resolvedType, typeToGenerate.Mode); + + object? discriminator = attributeData.ConstructorArguments.Length > 1 + ? attributeData.ConstructorArguments[1].Value + : null; + + (openGenericDerivedTypes ??= new()).Add(new PolymorphicDerivedTypeSpec + { + DerivedType = new TypeRef(resolvedType), + TypeDiscriminator = discriminator, + }); + } + else + { + ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeNotSupported, typeToGenerate.Location, derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString()); + } + } + else + { + EnqueueType(derivedType, typeToGenerate.Mode); + } if (!isPolymorphic && typeToGenerate.Mode == JsonSourceGenerationMode.Serialization) { @@ -859,6 +888,137 @@ private void ProcessTypeCustomAttributes( } } + /// + /// Resolves an unbound generic derived type to a closed type using the + /// type arguments from the constructed base type at compile time. + /// Returns null if the type arguments cannot be resolved. + /// + private static bool TryResolveOpenGenericDerivedType( + INamedTypeSymbol unboundDerived, + ITypeSymbol baseType, + [NotNullWhen(true)] out INamedTypeSymbol? resolvedType) + { + resolvedType = null; + + if (baseType is not INamedTypeSymbol { IsGenericType: true } constructedBase) + { + return false; + } + + INamedTypeSymbol derivedDefinition = unboundDerived.OriginalDefinition; + INamedTypeSymbol baseDefinition = constructedBase.OriginalDefinition; + + // Find the ancestor of the derived type definition that matches the base type definition. + INamedTypeSymbol? matchingBase = FindMatchingBaseType(derivedDefinition, baseDefinition); + if (matchingBase is null) + { + return false; + } + + var baseTypeArgs = constructedBase.TypeArguments; + var matchingBaseArgs = matchingBase.TypeArguments; + var derivedTypeParams = derivedDefinition.TypeParameters; + var resolved = new ITypeSymbol?[derivedTypeParams.Length]; + + // Unify the type arguments to build a mapping. + for (int i = 0; i < matchingBaseArgs.Length; i++) + { + if (!TryUnifyTypes(matchingBaseArgs[i], baseTypeArgs[i], derivedTypeParams, resolved)) + { + return false; + } + } + + // Verify all type parameters were resolved. + for (int i = 0; i < resolved.Length; i++) + { + if (resolved[i] is null) + { + return false; + } + } + + resolvedType = derivedDefinition.Construct(resolved!); + return true; + + static INamedTypeSymbol? FindMatchingBaseType(INamedTypeSymbol derivedDef, INamedTypeSymbol baseDef) + { + if (baseDef.TypeKind == TypeKind.Interface) + { + foreach (INamedTypeSymbol iface in derivedDef.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, baseDef)) + { + return iface; + } + } + } + else + { + for (INamedTypeSymbol? current = derivedDef.BaseType; current is not null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, baseDef)) + { + return current; + } + } + } + + return null; + } + + static bool TryUnifyTypes( + ITypeSymbol pattern, ITypeSymbol concrete, + System.Collections.Immutable.ImmutableArray typeParams, + ITypeSymbol?[] resolvedArgs) + { + if (pattern is ITypeParameterSymbol typeParam) + { + int index = typeParams.IndexOf(typeParam, SymbolEqualityComparer.Default); + if (index < 0) + { + return false; + } + + if (resolvedArgs[index] is null) + { + resolvedArgs[index] = concrete; + return true; + } + + return SymbolEqualityComparer.Default.Equals(resolvedArgs[index], concrete); + } + + if (pattern is INamedTypeSymbol { IsGenericType: true } namedPattern) + { + if (concrete is not INamedTypeSymbol { IsGenericType: true } namedConcrete) + { + return false; + } + + if (!SymbolEqualityComparer.Default.Equals(namedPattern.OriginalDefinition, namedConcrete.OriginalDefinition)) + { + return false; + } + + var patternArgs = namedPattern.TypeArguments; + var concreteArgs = namedConcrete.TypeArguments; + + for (int i = 0; i < patternArgs.Length; i++) + { + if (!TryUnifyTypes(patternArgs[i], concreteArgs[i], typeParams, resolvedArgs)) + { + return false; + } + } + + return true; + } + + return SymbolEqualityComparer.Default.Equals(pattern, concrete); + } + } + private bool TryResolveCollectionType( ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? valueType, diff --git a/src/libraries/System.Text.Json/gen/Model/PolymorphicDerivedTypeSpec.cs b/src/libraries/System.Text.Json/gen/Model/PolymorphicDerivedTypeSpec.cs new file mode 100644 index 00000000000000..72994c5168a6eb --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Model/PolymorphicDerivedTypeSpec.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SourceGenerators; + +namespace System.Text.Json.SourceGeneration +{ + /// + /// Models a resolved open generic derived type for polymorphic serialization. + /// + public sealed record PolymorphicDerivedTypeSpec + { + public required TypeRef DerivedType { get; init; } + + /// + /// The type discriminator, either a or value, or null. + /// + public required object? TypeDiscriminator { get; init; } + } +} diff --git a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs index 92bdbac52066c2..47df7ac4c9fc83 100644 --- a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs @@ -51,6 +51,13 @@ public sealed record TypeGenerationSpec public required bool IsPolymorphic { get; init; } + /// + /// Resolved open generic derived types for polymorphic serialization. + /// These are open generic [JsonDerivedType] declarations that have been + /// resolved to closed types at compile time using the base type's type arguments. + /// + public required ImmutableEquatableArray? OpenGenericDerivedTypes { 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 71ed2ca15e21bf..781d4424e8c036 100644 --- a/src/libraries/System.Text.Json/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/gen/Resources/Strings.resx @@ -207,4 +207,10 @@ The type '{0}' has been annotated with 'JsonIgnoreAttribute' using 'JsonIgnoreCondition.Always' which is not valid on type declarations. The attribute will be ignored. + + Open generic derived type is not resolvable for the polymorphic base type. + + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + 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 79b8cd0d6dd8ba..129908b23377cf 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 @@ -132,6 +132,16 @@ Typ obsahuje více členů s komentářem JsonExtensionDataAttribute + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. Typ {0} zahrnuje ref-like parametr vlastnosti, pole nebo konstruktoru {1}. Pro vlastnost, pole nebo konstruktor se nevygeneruje žádný zdrojový kód. 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 52b5e98696508a..d1499a25d2a0f1 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 @@ -132,6 +132,16 @@ Der Typ enthält mehrere Elemente, die mit dem JsonExtensionDataAttribute versehen sind. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. Der Typ „{0}“ enthält die Verweise wie Eigenschaft, Feld oder Konstruktorparameter „{1}“. Für die Eigenschaft, das Feld oder den Konstruktor wird kein Quellcode generiert. 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 0c75870edb1d11..23d1f2d3e4267b 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 @@ -132,6 +132,16 @@ El tipo tiene varios miembros anotados con JsonExtensionDataAttribute. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. El tipo "{0}" incluye una referencia como propiedad, campo o parámetro de constructor "{1}". No se generará código fuente para la propiedad, campo o constructor. 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 6ece010bcfd65a..3b88eb10c5e591 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 @@ -132,6 +132,16 @@ Le type comporte plusieurs membres annotés avec JsonExtensionDataAttribute. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. Le type « {0} » inclut le ref comme propriété, champ ou paramètre de constructeur « {1} ». Aucun code source ne sera généré pour la propriété, le champ ou le constructeur. 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 a542b8017265e5..cde61c230f5fd0 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 @@ -132,6 +132,16 @@ Nel tipo sono presenti più membri annotati con JsonExtensionDataAttribute. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. Il tipo '{0}' include un riferimento come la proprietà, il campo o il parametro del costruttore '{1}'. Non verrà generato codice sorgente per la proprietà, il campo o il costruttore. 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 27f29423ff6a5f..f3947bce538fad 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 @@ -132,6 +132,16 @@ 型には、'JsonExtensionDataAttribute' に注釈が付けられた複数のメンバーが含まれます。 + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. 型 '{0}' には、プロパティ、フィールド、コンストラクター パラメーター '{1}' などの ref が含まれます。プロパティ、フィールド、またはコンストラクターのソース コードは生成されません。 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 8832eb4d85a761..99bf103ab3bca3 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 @@ -132,6 +132,16 @@ 형식에 JsonExtensionDataAttribute로 주석이 추가 된 멤버가 여러 개 있습니다. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. '{0}' 형식에는 속성, 필드 또는 생성자 매개 변수 '{1}'와 같은 ref가 포함됩니다. 속성, 필드 또는 생성자에 대한 소스 코드가 생성되지 않습니다. 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 9cdc8b227b3532..a22b9d908d42e9 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 @@ -132,6 +132,16 @@ Typ ma wiele składowych opatrzonych adnotacjami za pomocą atrybutu JsonExtensionDataAttribute. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. Typ „{0}” zawiera parametr ref, taki jak właściwość, pole lub konstruktor „{1}”. Nie zostanie wygenerowany kod źródłowy dla właściwości, pola lub konstruktora. 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 3b74ad1d07dfde..3f182fd646720a 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 @@ -132,6 +132,16 @@ Tipo tem vários membros anotados com JsonExtensionDataAttribute. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. O tipo "{0}" inclui a propriedade ref like, campo ou parâmetro de construtor "{1}". Nenhum código-fonte será gerado para a propriedade, campo ou construtor. 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 bd9e003fac6361..20012f312f45b3 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 @@ -132,6 +132,16 @@ Тип содержит несколько элементов, помеченных с помощью JsonExtensionDataAttribute. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. Тип "{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 0757cda7d71717..3d24d32066d0d0 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 @@ -132,6 +132,16 @@ Tür, JsonExtensionDataAttribute ile açıklanan birden çok üyeye sahip. + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. '{0}' türü; özellik, alan veya oluşturucu parametresi '{1}' gibi başvuru içeriyor. Özellik, alan veya oluşturucu için kaynak kodu üretilmeyecek. 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 63e0251fec99f5..1607a8da9c63fa 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 @@ -132,6 +132,16 @@ 类型具有多个带有 JsonExtensionDataAttribute 注释的成员。 + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. 类型“{0}”包括属性、字段或构造函数参数“{1}”等引用。不会为属性、字段或构造函数生成源代码。 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 95061df16ecbb5..c65fcbcc6ee622 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 @@ -132,6 +132,16 @@ 類型具有使用 JsonExtensionDataAttribute 標註的多個成員。 + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. + + + + Open generic derived type is not resolvable for the polymorphic base type. + Open generic derived type is not resolvable for the polymorphic base type. + + The type '{0}' includes the ref like property, field or constructor parameter '{1}'. No source code will be generated for the property, field or constructor. 類型 '{0}' 包含 ref,例如屬性、欄位或建構函式參數 '{1}'。不會針對屬性、欄位或建構函式產生原始程式碼。 diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets index 0c7c4cead803df..abdaa73bfb0823 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets @@ -73,6 +73,7 @@ + diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index b67fc02b7a5a86..f84919fa616173 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -660,10 +660,13 @@ The converter for derived type '{0}' does not support metadata writes or reads. - Specified type '{0}' does not support polymorphism. Polymorphic types cannot be structs, sealed types, generic types or System.Object. + Specified type '{0}' does not support polymorphism. Polymorphic types cannot be structs, sealed types, open generic type definitions or System.Object. - Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type, must not be generic and cannot be abstract classes or interfaces unless 'JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor' is specified. + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type, must not be open generic type definitions and cannot be abstract classes or interfaces unless 'JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor' is specified. + + + The open generic type '{0}' cannot be used as a derived type for the polymorphic type '{1}'. The type arguments of the open generic derived type must be resolvable from the base type's generic arguments. The polymorphic type '{0}' has already specified derived type '{1}'. 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 697bdc2b7b7300..cb75333dfb0771 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 @@ -64,6 +64,7 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte } typeInfo.PopulatePolymorphismMetadata(); + ResolveOpenGenericDerivedTypes(typeInfo); typeInfo.MapInterfaceTypesToCallbacks(); Func? createObject = DetermineCreateObjectDelegate(type, converter); @@ -640,5 +641,189 @@ static NullabilityState TranslateByte(byte b) => NullabilityInfo nullability = nullabilityCtx.Create(parameterInfo); return nullability.WriteState; } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static void ResolveOpenGenericDerivedTypes(JsonTypeInfo typeInfo) + { + Type baseType = typeInfo.Type; + + bool hasOpenGenericDerivedTypes = false; + foreach (JsonDerivedTypeAttribute attr in baseType.GetCustomAttributes(inherit: false)) + { + if (attr.DerivedType is { IsGenericTypeDefinition: true }) + { + hasOpenGenericDerivedTypes = true; + break; + } + } + + if (!hasOpenGenericDerivedTypes) + { + return; + } + + if (!baseType.IsGenericType) + { + // Non-generic base with open generic derived types — always an error. + foreach (JsonDerivedTypeAttribute attr in baseType.GetCustomAttributes(inherit: false)) + { + if (attr.DerivedType is { IsGenericTypeDefinition: true }) + { + ThrowHelper.ThrowInvalidOperationException_OpenGenericDerivedTypeNotSupported(baseType, attr.DerivedType); + } + } + + return; + } + + Type baseTypeDefinition = baseType.GetGenericTypeDefinition(); + Type[] baseTypeArgs = baseType.GetGenericArguments(); + + foreach (JsonDerivedTypeAttribute attr in baseType.GetCustomAttributes(inherit: false)) + { + if (attr.DerivedType is not { IsGenericTypeDefinition: true }) + { + continue; + } + + Type derivedType = attr.DerivedType; + if (!TryResolveOpenGenericDerivedType(derivedType, baseTypeDefinition, baseTypeArgs, out Type? resolvedType)) + { + ThrowHelper.ThrowInvalidOperationException_OpenGenericDerivedTypeNotSupported(baseType, derivedType); + } + + JsonPolymorphismOptions options = typeInfo.PolymorphismOptions ??= new(); + options.DerivedTypes.Add(new JsonDerivedType(resolvedType, attr.TypeDiscriminator)); + } + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static bool TryResolveOpenGenericDerivedType( + Type openDerivedType, + Type baseTypeDefinition, + Type[] baseTypeArgs, + [NotNullWhen(true)] out Type? closedDerivedType) + { + closedDerivedType = null; + + // Find the ancestor of the open derived type that matches the base type definition. + Type? matchingBase = FindMatchingBaseType(openDerivedType, baseTypeDefinition); + if (matchingBase is null) + { + return false; + } + + Type[] matchingBaseArgs = matchingBase.GetGenericArguments(); + Debug.Assert(matchingBaseArgs.Length == baseTypeArgs.Length); + + // Build a mapping from the derived type's generic parameters to concrete types. + Type[] derivedTypeParams = openDerivedType.GetGenericArguments(); + Type?[] resolvedArgs = new Type?[derivedTypeParams.Length]; + + for (int i = 0; i < matchingBaseArgs.Length; i++) + { + if (!TryUnifyTypes(matchingBaseArgs[i], baseTypeArgs[i], derivedTypeParams, resolvedArgs)) + { + return false; + } + } + + // Verify all type parameters were resolved. + for (int i = 0; i < resolvedArgs.Length; i++) + { + if (resolvedArgs[i] is null) + { + return false; + } + } + + try + { + closedDerivedType = openDerivedType.MakeGenericType(resolvedArgs!); + return true; + } + catch (ArgumentException) + { + // Type constraints were violated. + return false; + } + + static Type? FindMatchingBaseType(Type derivedType, Type baseTypeDefinition) + { + if (baseTypeDefinition.IsInterface) + { + foreach (Type iface in derivedType.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == baseTypeDefinition) + { + return iface; + } + } + } + else + { + for (Type? current = derivedType.BaseType; current is not null; current = current.BaseType) + { + if (current.IsGenericType && current.GetGenericTypeDefinition() == baseTypeDefinition) + { + return current; + } + } + } + + return null; + } + + static bool TryUnifyTypes(Type pattern, Type concrete, Type[] typeParams, Type?[] resolvedArgs) + { + if (pattern.IsGenericParameter) + { + int index = Array.IndexOf(typeParams, pattern); + if (index < 0) + { + return false; + } + + if (resolvedArgs[index] is null) + { + resolvedArgs[index] = concrete; + return true; + } + + return resolvedArgs[index] == concrete; + } + + if (pattern.IsGenericType) + { + if (!concrete.IsGenericType) + { + return false; + } + + if (pattern.GetGenericTypeDefinition() != concrete.GetGenericTypeDefinition()) + { + return false; + } + + Type[] patternArgs = pattern.GetGenericArguments(); + Type[] concreteArgs = concrete.GetGenericArguments(); + + for (int i = 0; i < patternArgs.Length; i++) + { + if (!TryUnifyTypes(patternArgs[i], concreteArgs[i], typeParams, resolvedArgs)) + { + return false; + } + } + + return true; + } + + // Non-generic, non-parameter type: must be an exact match. + return pattern == concrete; + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPolymorphismOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPolymorphismOptions.cs index de6f6993edd835..172df7397ddd45 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPolymorphismOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPolymorphismOptions.cs @@ -114,6 +114,13 @@ public DerivedTypeList(JsonPolymorphismOptions parent) foreach (JsonDerivedTypeAttribute attr in baseType.GetCustomAttributes(inherit: false)) { + // Skip open generic derived types; they are resolved separately + // by the DefaultJsonTypeInfoResolver (reflection) or source generator (AOT). + if (attr.DerivedType is { IsGenericTypeDefinition: true }) + { + continue; + } + (options ??= new()).DerivedTypes.Add(new JsonDerivedType(attr.DerivedType, attr.TypeDiscriminator)); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 400817503b9bdc..3baca8482b39ac 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -1235,6 +1235,14 @@ internal void PopulatePolymorphismMetadata() { Debug.Assert(!IsReadOnly); + // If polymorphism options have already been set by the source generator, + // skip attribute-based resolution. Preserve the attribute logic for + // binary compatibility with source generated code from earlier versions. + if (_polymorphismOptions is not null) + { + return; + } + JsonPolymorphismOptions? options = JsonPolymorphismOptions.CreateFromAttributeDeclarations(Type); if (options != null) { 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 afccfa57946fe1..33035deda8a124 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 @@ -949,6 +949,12 @@ public static void ThrowInvalidOperationException_DerivedTypeNotSupported(Type b throw new InvalidOperationException(SR.Format(SR.Polymorphism_DerivedTypeIsNotSupported, derivedType, baseType)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_OpenGenericDerivedTypeNotSupported(Type baseType, Type derivedType) + { + throw new InvalidOperationException(SR.Format(SR.Polymorphism_OpenGenericDerivedTypeNotSupported, derivedType, baseType)); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_DerivedTypeIsAlreadySpecified(Type baseType, Type derivedType) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonPolymorphismOptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonPolymorphismOptionsTests.cs index d0d6bd48862071..9e2613e78db63b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonPolymorphismOptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonPolymorphismOptionsTests.cs @@ -136,9 +136,26 @@ public static void DefaultResolver_ReportsCorrectPolymorphismMetadata(Type polym Assert.Equal(polymorphicAttribute?.IgnoreUnrecognizedTypeDiscriminators ?? false, polyOptions.IgnoreUnrecognizedTypeDiscriminators); Assert.Equal(polymorphicAttribute?.UnknownDerivedTypeHandling ?? default, polyOptions.UnknownDerivedTypeHandling); Assert.Equal(polymorphicAttribute?.TypeDiscriminatorPropertyName ?? "$type", polyOptions.TypeDiscriminatorPropertyName); - Assert.Equal( - expected: derivedTypeAttributes.Select(attr => (attr.DerivedType, attr.TypeDiscriminator)), - actual: polyOptions.DerivedTypes.Select(attr => (attr.DerivedType, attr.TypeDiscriminator))); + Assert.Equal(derivedTypeAttributes.Length, polyOptions.DerivedTypes.Count); + + for (int i = 0; i < derivedTypeAttributes.Length; i++) + { + JsonDerivedTypeAttribute attr = derivedTypeAttributes[i]; + JsonDerivedType derivedType = polyOptions.DerivedTypes[i]; + Assert.Equal(attr.TypeDiscriminator, derivedType.TypeDiscriminator); + + if (attr.DerivedType is { IsGenericTypeDefinition: true } || + (attr.DerivedType is { DeclaringType: { IsGenericTypeDefinition: true } })) + { + // Open generic derived types are resolved by the reflection resolver + // to closed types, so we can't compare them directly. + Assert.False(derivedType.DerivedType.IsGenericTypeDefinition); + } + else + { + Assert.Equal(attr.DerivedType, derivedType.DerivedType); + } + } } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs index 3951eb4adac365..be3caa0a59ca57 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs @@ -2638,17 +2638,21 @@ public class B : PolymorphicClasWithDuplicateTypeDiscriminators { } } [Fact] - public async Task PolymorphicGenericClass_ThrowsInvalidOperationException() + public async Task PolymorphicGenericClass_SupportsOpenGenericDerivedType() { - PolymorphicGenericClass value = new PolymorphicGenericClass.DerivedClass(); - await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + PolymorphicGenericClass value = new PolymorphicGenericClass.DerivedClass { BaseValue = 1, DerivedValue = 2 }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"BaseValue":1,"DerivedValue":2}""", json); } [JsonDerivedType(typeof(PolymorphicGenericClass<>.DerivedClass))] public class PolymorphicGenericClass { + public T? BaseValue { get; set; } + public class DerivedClass : PolymorphicGenericClass { + public T? DerivedValue { get; set; } } } @@ -2667,6 +2671,249 @@ public class DerivedClass : PolymorphicDerivedGenericClass } } + #region Open Generic Polymorphism Tests + + [Fact] + public async Task OpenGenericDerivedType_WithStringDiscriminator_SerializationWorks() + { + OpenGenericBase_StringDisc value = new OpenGenericDerived_StringDisc { Value = 42 }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Value":42}""", json); + } + + [Fact] + public async Task OpenGenericDerivedType_WithStringDiscriminator_DeserializationWorks() + { + string json = """{"$type":"derived","Value":42}"""; + var result = await Serializer.DeserializeWrapper>(json); + Assert.IsType>(result); + Assert.Equal(42, ((OpenGenericDerived_StringDisc)result).Value); + } + + [JsonDerivedType(typeof(OpenGenericDerived_StringDisc<>), "derived")] + public class OpenGenericBase_StringDisc + { + public T? Value { get; set; } + } + + public class OpenGenericDerived_StringDisc : OpenGenericBase_StringDisc; + + [Fact] + public async Task OpenGenericDerivedType_WithIntDiscriminator_SerializationWorks() + { + OpenGenericBase_IntDisc value = new OpenGenericDerived_IntDisc { Value = "hello" }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":1,"Value":"hello"}""", json); + } + + [Fact] + public async Task OpenGenericDerivedType_WithIntDiscriminator_DeserializationWorks() + { + string json = """{"$type":1,"Value":"hello"}"""; + var result = await Serializer.DeserializeWrapper>(json); + Assert.IsType>(result); + Assert.Equal("hello", ((OpenGenericDerived_IntDisc)result).Value); + } + + [JsonDerivedType(typeof(OpenGenericDerived_IntDisc<>), 1)] + public class OpenGenericBase_IntDisc + { + public T? Value { get; set; } + } + + public class OpenGenericDerived_IntDisc : OpenGenericBase_IntDisc; + + [Fact] + public async Task OpenGenericDerivedType_MultipleDerivedTypes_Work() + { + OpenGenericBase_Multi valueA = new OpenGenericDerivedA_Multi { ValueA = 1 }; + OpenGenericBase_Multi valueB = new OpenGenericDerivedB_Multi { ValueB = 2 }; + + string jsonA = await Serializer.SerializeWrapper(valueA); + string jsonB = await Serializer.SerializeWrapper(valueB); + + JsonTestHelper.AssertJsonEqual("""{"$type":"a","ValueA":1}""", jsonA); + JsonTestHelper.AssertJsonEqual("""{"$type":"b","ValueB":2}""", jsonB); + + var resultA = await Serializer.DeserializeWrapper>(jsonA); + var resultB = await Serializer.DeserializeWrapper>(jsonB); + + Assert.IsType>(resultA); + Assert.IsType>(resultB); + } + + [JsonDerivedType(typeof(OpenGenericDerivedA_Multi<>), "a")] + [JsonDerivedType(typeof(OpenGenericDerivedB_Multi<>), "b")] + public class OpenGenericBase_Multi; + + public class OpenGenericDerivedA_Multi : OpenGenericBase_Multi + { + public int ValueA { get; set; } + } + + public class OpenGenericDerivedB_Multi : OpenGenericBase_Multi + { + public int ValueB { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_NestedClass_Works() + { + OpenGenericBase_Nested value = new OpenGenericBase_Nested.Derived(); + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"nested"}""", json); + } + + [JsonDerivedType(typeof(OpenGenericBase_Nested<>.Derived), "nested")] + public class OpenGenericBase_Nested + { + public class Derived : OpenGenericBase_Nested; + } + + [Fact] + public async Task OpenGenericDerivedType_ComplexTypeArg_Works() + { + OpenGenericBase_ComplexArg> value = new OpenGenericDerived_ComplexArg> { Data = [1, 2, 3] }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Data":[1,2,3]}""", json); + } + + [JsonDerivedType(typeof(OpenGenericDerived_ComplexArg<>), "derived")] + public class OpenGenericBase_ComplexArg + { + public T? Data { get; set; } + } + + public class OpenGenericDerived_ComplexArg : OpenGenericBase_ComplexArg; + + [Fact] + public async Task OpenGenericDerivedType_WrappedTypeArg_Works() + { + // Derived : Base> - the type args are related but not identical + OpenGenericBase_Wrapped> value = new OpenGenericDerived_Wrapped { Data = ["a", "b"] }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Data":["a","b"]}""", json); + + var result = await Serializer.DeserializeWrapper>>(json); + Assert.IsType>(result); + } + + [JsonDerivedType(typeof(OpenGenericDerived_Wrapped<>), "derived")] + public class OpenGenericBase_Wrapped + { + public T? Data { get; set; } + } + + public class OpenGenericDerived_Wrapped : OpenGenericBase_Wrapped>; + + [Fact] + public async Task OpenGenericDerivedType_Interface_Works() + { + IOpenGenericBase value = new OpenGenericInterfaceImpl { Value = 42 }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"impl","Value":42}""", json); + + var result = await Serializer.DeserializeWrapper>(json); + Assert.IsType>(result); + } + + [JsonDerivedType(typeof(OpenGenericInterfaceImpl<>), "impl")] + public interface IOpenGenericBase + { + T? Value { get; set; } + } + + public class OpenGenericInterfaceImpl : IOpenGenericBase + { + public T? Value { get; set; } + } + + [Fact] + public async Task OpenGenericDerivedType_DifferentTypeArguments_ProduceDifferentResults() + { + OpenGenericBase_StringDisc intValue = new OpenGenericDerived_StringDisc { Value = 42 }; + OpenGenericBase_StringDisc strValue = new OpenGenericDerived_StringDisc { Value = "hello" }; + + string intJson = await Serializer.SerializeWrapper(intValue); + string strJson = await Serializer.SerializeWrapper(strValue); + + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Value":42}""", intJson); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Value":"hello"}""", strJson); + } + + [Fact] + public async Task OpenGenericDerivedType_NonGenericBase_ThrowsInvalidOperationException() + { + var value = new NonGenericBaseWithOpenGenericDerived(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(NonGenericBaseWithOpenGenericDerived.OpenDerived<>), "derived")] + public class NonGenericBaseWithOpenGenericDerived + { + public class OpenDerived : NonGenericBaseWithOpenGenericDerived; + } + + [Fact] + public async Task OpenGenericDerivedType_TypeArgsNotResolvable_ThrowsInvalidOperationException() + { + // Derived : Base - T cannot be determined from Base + var value = new OpenGenericBase_Unresolvable(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(OpenGenericDerived_Unresolvable<>), "derived")] + public class OpenGenericBase_Unresolvable + { + public T? Value { get; set; } + } + + public class OpenGenericDerived_Unresolvable : OpenGenericBase_Unresolvable; + + [Fact] + public async Task OpenGenericDerivedType_ProgrammaticApi_Works() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Type == typeof(OpenGenericBase_Programmatic)) + { + typeInfo.PolymorphismOptions = new JsonPolymorphismOptions + { + DerivedTypes = + { + new JsonDerivedType(typeof(OpenGenericDerived_Programmatic), "derived"), + } + }; + } + } + } + } + }; + + OpenGenericBase_Programmatic value = new OpenGenericDerived_Programmatic { Value = 99 }; + string json = JsonSerializer.Serialize(value, options); + JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Value":99}""", json); + + var result = JsonSerializer.Deserialize>(json, options); + Assert.IsType>(result); + Assert.Equal(99, ((OpenGenericDerived_Programmatic)result).Value); + } + + public class OpenGenericBase_Programmatic + { + public T? Value { get; set; } + } + + public class OpenGenericDerived_Programmatic : OpenGenericBase_Programmatic; + + #endregion + [Fact] public async Task PolymorphicClass_CustomConverter_TypeDiscriminator_Serialization_ThrowsNotSupportedException() {