Skip to content
2 changes: 1 addition & 1 deletion docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
23 changes: 23 additions & 0 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down
166 changes: 163 additions & 3 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PolymorphicDerivedTypeSpec>? openGenericDerivedTypes);

if (type is { IsRefLikeType: true } or INamedTypeSymbol { IsUnboundGenericType: true } or IErrorTypeSymbol)
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -770,7 +772,8 @@ private void ProcessTypeCustomAttributes(
out JsonIgnoreCondition? typeIgnoreCondition,
out bool foundJsonConverterAttribute,
out TypeRef? customConverterType,
out bool isPolymorphic)
out bool isPolymorphic,
out List<PolymorphicDerivedTypeSpec>? openGenericDerivedTypes)
{
numberHandling = null;
unmappedMemberHandling = null;
Expand All @@ -780,6 +783,7 @@ private void ProcessTypeCustomAttributes(
customConverterType = null;
foundJsonConverterAttribute = false;
isPolymorphic = false;
openGenericDerivedTypes = null;

foreach (AttributeData attributeData in typeToGenerate.Type.GetAttributes())
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -859,6 +888,137 @@ private void ProcessTypeCustomAttributes(
}
}

/// <summary>
/// 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.
/// </summary>
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<ITypeParameterSymbol> 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Models a resolved open generic derived type for polymorphic serialization.
/// </summary>
public sealed record PolymorphicDerivedTypeSpec
{
public required TypeRef DerivedType { get; init; }

/// <summary>
/// The type discriminator, either a <see cref="string"/> or <see cref="int"/> value, or null.
/// </summary>
public required object? TypeDiscriminator { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ public sealed record TypeGenerationSpec

public required bool IsPolymorphic { get; init; }

/// <summary>
/// Resolved open generic derived types for polymorphic serialization.
/// These are open generic <c>[JsonDerivedType]</c> declarations that have been
/// resolved to closed types at compile time using the base type's type arguments.
/// </summary>
public required ImmutableEquatableArray<PolymorphicDerivedTypeSpec>? OpenGenericDerivedTypes { get; init; }

public required bool IsValueTuple { get; init; }

public required JsonNumberHandling? NumberHandling { get; init; }
Expand Down
6 changes: 6 additions & 0 deletions src/libraries/System.Text.Json/gen/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,10 @@
<data name="JsonIgnoreConditionAlwaysInvalidOnTypeFormat" xml:space="preserve">
<value>The type '{0}' has been annotated with 'JsonIgnoreAttribute' using 'JsonIgnoreCondition.Always' which is not valid on type declarations. The attribute will be ignored.</value>
</data>
<data name="OpenGenericDerivedTypeNotSupportedTitle" xml:space="preserve">
<value>Open generic derived type is not resolvable for the polymorphic base type.</value>
</data>
<data name="OpenGenericDerivedTypeNotSupportedMessageFormat" xml:space="preserve">
<value>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.</value>
</data>
</root>
10 changes: 10 additions & 0 deletions src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@
<target state="translated">Typ obsahuje více členů s komentářem JsonExtensionDataAttribute</target>
<note />
</trans-unit>
<trans-unit id="OpenGenericDerivedTypeNotSupportedMessageFormat">
<source>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.</source>
<target state="new">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.</target>
<note />
</trans-unit>
<trans-unit id="OpenGenericDerivedTypeNotSupportedTitle">
<source>Open generic derived type is not resolvable for the polymorphic base type.</source>
<target state="new">Open generic derived type is not resolvable for the polymorphic base type.</target>
<note />
</trans-unit>
<trans-unit id="TypeContainsRefLikeMemberFormat">
<source>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.</source>
<target state="translated">Typ {0} zahrnuje ref-like parametr vlastnosti, pole nebo konstruktoru {1}. Pro vlastnost, pole nebo konstruktor se nevygeneruje žádný zdrojový kód.</target>
Expand Down
10 changes: 10 additions & 0 deletions src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@
<target state="translated">Der Typ enthält mehrere Elemente, die mit dem JsonExtensionDataAttribute versehen sind.</target>
<note />
</trans-unit>
<trans-unit id="OpenGenericDerivedTypeNotSupportedMessageFormat">
<source>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.</source>
<target state="new">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.</target>
<note />
</trans-unit>
<trans-unit id="OpenGenericDerivedTypeNotSupportedTitle">
<source>Open generic derived type is not resolvable for the polymorphic base type.</source>
<target state="new">Open generic derived type is not resolvable for the polymorphic base type.</target>
<note />
</trans-unit>
<trans-unit id="TypeContainsRefLikeMemberFormat">
<source>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.</source>
<target state="translated">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.</target>
Expand Down
Loading