Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 123 additions & 7 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,7 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
ProcessMemberCustomAttributes(
contextType,
memberInfo,
memberType,
out bool hasJsonInclude,
out string? jsonPropertyName,
out JsonIgnoreCondition? ignoreCondition,
Expand Down Expand Up @@ -1322,6 +1323,7 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
private void ProcessMemberCustomAttributes(
INamedTypeSymbol contextType,
ISymbol memberInfo,
ITypeSymbol memberType,
out bool hasJsonInclude,
out string? jsonPropertyName,
out JsonIgnoreCondition? ignoreCondition,
Expand Down Expand Up @@ -1355,7 +1357,7 @@ private void ProcessMemberCustomAttributes(

if (converterType is null && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
{
converterType = GetConverterTypeFromJsonConverterAttribute(contextType, memberInfo, attributeData);
converterType = GetConverterTypeFromJsonConverterAttribute(contextType, memberInfo, attributeData, memberType);
}
else if (attributeType.ContainingAssembly.Name == SystemTextJsonNamespace)
{
Expand Down Expand Up @@ -1657,7 +1659,7 @@ bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec)
return propertyInitializers;
}

private TypeRef? GetConverterTypeFromJsonConverterAttribute(INamedTypeSymbol contextType, ISymbol declaringSymbol, AttributeData attributeData)
private TypeRef? GetConverterTypeFromJsonConverterAttribute(INamedTypeSymbol contextType, ISymbol declaringSymbol, AttributeData attributeData, ITypeSymbol? typeToConvert = null)
{
Debug.Assert(_knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeData.AttributeClass));

Expand All @@ -1669,25 +1671,139 @@ bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec)

Debug.Assert(attributeData.ConstructorArguments.Length == 1 && attributeData.ConstructorArguments[0].Value is null or ITypeSymbol);
var converterType = (ITypeSymbol?)attributeData.ConstructorArguments[0].Value;
return GetConverterTypeFromAttribute(contextType, converterType, declaringSymbol, attributeData);

// If typeToConvert is not provided, try to infer it from declaringSymbol
typeToConvert ??= declaringSymbol as ITypeSymbol;

return GetConverterTypeFromAttribute(contextType, converterType, declaringSymbol, attributeData, typeToConvert);
}

private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, ITypeSymbol? converterType, ISymbol declaringSymbol, AttributeData attributeData)
private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, ITypeSymbol? converterType, ISymbol declaringSymbol, AttributeData attributeData, ITypeSymbol? typeToConvert = null)
{
if (converterType is not INamedTypeSymbol namedConverterType ||
INamedTypeSymbol? namedConverterType = converterType as INamedTypeSymbol;

// Check if this is an unbound generic converter type that needs to be constructed.
// For open generics, we construct the closed generic type first and then validate.
if (namedConverterType is { IsUnboundGenericType: true } unboundConverterType &&
typeToConvert is INamedTypeSymbol { IsGenericType: true } genericTypeToConvert)
{
// For nested generic types like Container<>.NestedConverter<>, we need to count
// all type parameters from the entire type hierarchy, not just the immediate type.
int totalTypeParameterCount = GetTotalTypeParameterCount(unboundConverterType);

if (totalTypeParameterCount == genericTypeToConvert.TypeArguments.Length)
{
namedConverterType = ConstructNestedGenericType(unboundConverterType, genericTypeToConvert.TypeArguments);
}
}

if (namedConverterType is null ||
!_knownSymbols.JsonConverterType.IsAssignableFrom(namedConverterType) ||
!namedConverterType.Constructors.Any(c => c.Parameters.Length == 0 && IsSymbolAccessibleWithin(c, within: contextType)))
{
ReportDiagnostic(DiagnosticDescriptors.JsonConverterAttributeInvalidType, attributeData.GetLocation(), converterType?.ToDisplayString() ?? "null", declaringSymbol.ToDisplayString());
return null;
}

if (_knownSymbols.JsonStringEnumConverterType.IsAssignableFrom(converterType))
if (_knownSymbols.JsonStringEnumConverterType.IsAssignableFrom(namedConverterType))
{
ReportDiagnostic(DiagnosticDescriptors.JsonStringEnumConverterNotSupportedInAot, attributeData.GetLocation(), declaringSymbol.ToDisplayString());
}

return new TypeRef(converterType);
return new TypeRef(namedConverterType);
}

/// <summary>
/// Gets the total number of type parameters from an unbound generic type,
/// including type parameters from containing types for nested generics.
/// For example, Container&lt;&gt;.NestedConverter&lt;&gt; has a total of 2 type parameters.
/// </summary>
private static int GetTotalTypeParameterCount(INamedTypeSymbol unboundType)
{
int count = 0;
INamedTypeSymbol? current = unboundType;
while (current != null)
{
count += current.TypeParameters.Length;
current = current.ContainingType;
}
return count;
}

/// <summary>
/// Constructs a closed generic type from an unbound generic type (potentially nested),
/// using the provided type arguments in the order they should be applied.
/// Returns null if the type cannot be constructed.
/// </summary>
private static INamedTypeSymbol? ConstructNestedGenericType(INamedTypeSymbol unboundType, ImmutableArray<ITypeSymbol> typeArguments)
{
// Build the chain of containing types from outermost to innermost
var typeChain = new List<INamedTypeSymbol>();
INamedTypeSymbol? current = unboundType;
while (current != null)
{
typeChain.Add(current);
current = current.ContainingType;
}

// Reverse to go from outermost to innermost
typeChain.Reverse();

// Track which type arguments have been used
int typeArgIndex = 0;
INamedTypeSymbol? constructedContainingType = null;

foreach (var type in typeChain)
{
int typeParamCount = type.TypeParameters.Length;
INamedTypeSymbol originalDef = type.OriginalDefinition;

if (typeParamCount > 0)
{
// Get the type arguments for this level
var args = typeArguments.Skip(typeArgIndex).Take(typeParamCount).ToArray();
typeArgIndex += typeParamCount;

// Construct this level
if (constructedContainingType == null)
{
constructedContainingType = originalDef.Construct(args);
}
else
{
// Get the nested type from the constructed containing type
var nestedTypeDef = constructedContainingType.GetTypeMembers(originalDef.Name, originalDef.Arity).FirstOrDefault();
if (nestedTypeDef != null)
{
constructedContainingType = nestedTypeDef.Construct(args);
}
else
{
return null;
}
}
}
else
{
// Non-generic type in the chain
if (constructedContainingType == null)
{
constructedContainingType = originalDef;
}
else
{
// Use arity 0 to avoid ambiguity with nested types of the same name but different arity
var nestedType = constructedContainingType.GetTypeMembers(originalDef.Name, 0).FirstOrDefault();
if (nestedType == null)
{
return null;
}
constructedContainingType = nestedType;
}
}
}

return constructedContainingType;
}

private static string DetermineEffectiveJsonPropertyName(string propertyName, string? jsonPropertyName, SourceGenerationOptionsSpec? options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,16 @@ private static JsonConverter GetConverterFromAttribute(JsonConverterAttribute co
}
else
{
// Handle open generic converter types (e.g., OptionConverter<> on Option<T>).
// If the converter type is an open generic and the type to convert is a closed generic
// with matching type arity, construct the closed converter type.
if (converterType.IsGenericTypeDefinition &&
typeToConvert.IsGenericType &&
converterType.GetGenericArguments().Length == typeToConvert.GetGenericArguments().Length)
{
converterType = converterType.MakeGenericType(typeToConvert.GetGenericArguments());
}

ConstructorInfo? ctor = converterType.GetConstructor(Type.EmptyTypes);
if (!typeof(JsonConverter).IsAssignableFrom(converterType) || ctor == null || !ctor.IsPublic)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1024,5 +1024,145 @@ public static void PartialContextWithAttributesOnMultipleDeclarations_RuntimeBeh
Assert.Equal(3.14, deserialized2.Value);
Assert.True(deserialized2.IsActive);
}

[Theory]
[InlineData(42, "42")]
[InlineData(0, "0")]
public static void SupportsOpenGenericConverterOnGenericType_Int(int value, string expectedJson)
{
Option<int> option = new Option<int>(value);
string json = JsonSerializer.Serialize(option, OpenGenericConverterContext.Default.OptionInt32);
Assert.Equal(expectedJson, json);

Option<int> deserialized = JsonSerializer.Deserialize<Option<int>>(json, OpenGenericConverterContext.Default.OptionInt32);
Assert.True(deserialized.HasValue);
Assert.Equal(value, deserialized.Value);
}

[Fact]
public static void SupportsOpenGenericConverterOnGenericType_NullValue()
{
Option<int> option = default;
string json = JsonSerializer.Serialize(option, OpenGenericConverterContext.Default.OptionInt32);
Assert.Equal("null", json);

Option<int> deserialized = JsonSerializer.Deserialize<Option<int>>("null", OpenGenericConverterContext.Default.OptionInt32);
Assert.False(deserialized.HasValue);
}

[Theory]
[InlineData("hello", @"""hello""")]
[InlineData("", @"""""")]
public static void SupportsOpenGenericConverterOnGenericType_String(string value, string expectedJson)
{
Option<string> option = new Option<string>(value);
string json = JsonSerializer.Serialize(option, OpenGenericConverterContext.Default.OptionString);
Assert.Equal(expectedJson, json);

Option<string> deserialized = JsonSerializer.Deserialize<Option<string>>(json, OpenGenericConverterContext.Default.OptionString);
Assert.True(deserialized.HasValue);
Assert.Equal(value, deserialized.Value);
}

[Fact]
public static void SupportsOpenGenericConverterOnProperty()
{
var obj = new ClassWithGenericConverterOnProperty { Value = new GenericWrapper<int>(42) };
string json = JsonSerializer.Serialize(obj, OpenGenericConverterContext.Default.ClassWithGenericConverterOnProperty);
Assert.Equal(@"{""Value"":42}", json);

var deserialized = JsonSerializer.Deserialize<ClassWithGenericConverterOnProperty>(json, OpenGenericConverterContext.Default.ClassWithGenericConverterOnProperty);
Assert.Equal(42, deserialized.Value.WrappedValue);
}

[JsonSerializable(typeof(Option<int>))]
[JsonSerializable(typeof(Option<string>))]
[JsonSerializable(typeof(ClassWithOptionProperty))]
[JsonSerializable(typeof(ClassWithGenericConverterOnProperty))]
internal partial class OpenGenericConverterContext : JsonSerializerContext
{
}

[Fact]
public static void SupportsNestedGenericConverterOnGenericType()
{
var value = new TypeWithNestedConverter<int, string> { Value1 = 42, Value2 = "hello" };
string json = JsonSerializer.Serialize(value, NestedGenericConverterContext.Default.TypeWithNestedConverterInt32String);
Assert.Equal(@"{""Value1"":42,""Value2"":""hello""}", json);

var deserialized = JsonSerializer.Deserialize<TypeWithNestedConverter<int, string>>(json, NestedGenericConverterContext.Default.TypeWithNestedConverterInt32String);
Assert.Equal(42, deserialized.Value1);
Assert.Equal("hello", deserialized.Value2);
}

[Fact]
public static void SupportsConstrainedGenericConverterOnGenericType()
{
var value = new TypeWithSatisfiedConstraint<string> { Value = "test" };
string json = JsonSerializer.Serialize(value, NestedGenericConverterContext.Default.TypeWithSatisfiedConstraintString);
Assert.Equal(@"{""Value"":""test""}", json);

var deserialized = JsonSerializer.Deserialize<TypeWithSatisfiedConstraint<string>>(json, NestedGenericConverterContext.Default.TypeWithSatisfiedConstraintString);
Assert.Equal("test", deserialized.Value);
}

[Fact]
public static void SupportsGenericWithinNonGenericWithinGenericConverter()
{
var value = new TypeWithDeeplyNestedConverter<int, string> { Value1 = 99, Value2 = "deep" };
string json = JsonSerializer.Serialize(value, NestedGenericConverterContext.Default.TypeWithDeeplyNestedConverterInt32String);
Assert.Equal(@"{""Value1"":99,""Value2"":""deep""}", json);

var deserialized = JsonSerializer.Deserialize<TypeWithDeeplyNestedConverter<int, string>>(json, NestedGenericConverterContext.Default.TypeWithDeeplyNestedConverterInt32String);
Assert.Equal(99, deserialized.Value1);
Assert.Equal("deep", deserialized.Value2);
}

[Fact]
public static void SupportsSingleGenericLevelNestedConverter()
{
var value = new TypeWithSingleLevelNestedConverter<int> { Value = 42 };
string json = JsonSerializer.Serialize(value, NestedGenericConverterContext.Default.TypeWithSingleLevelNestedConverterInt32);
Assert.Equal(@"{""Value"":42}", json);

var deserialized = JsonSerializer.Deserialize<TypeWithSingleLevelNestedConverter<int>>(json, NestedGenericConverterContext.Default.TypeWithSingleLevelNestedConverterInt32);
Assert.Equal(42, deserialized.Value);
}

[Fact]
public static void SupportsAsymmetricNestedConverterWithManyParams()
{
var value = new TypeWithManyParams<int, string, bool, double, long>
{
Value1 = 1,
Value2 = "two",
Value3 = true,
Value4 = 4.0,
Value5 = 5L
};
string json = JsonSerializer.Serialize(value, NestedGenericConverterContext.Default.TypeWithManyParamsInt32StringBooleanDoubleInt64);
Assert.Equal(@"{""Value1"":1,""Value2"":""two"",""Value3"":true,""Value4"":4,""Value5"":5}", json);

var deserialized = JsonSerializer.Deserialize<TypeWithManyParams<int, string, bool, double, long>>(json, NestedGenericConverterContext.Default.TypeWithManyParamsInt32StringBooleanDoubleInt64);
Assert.Equal(1, deserialized.Value1);
Assert.Equal("two", deserialized.Value2);
Assert.True(deserialized.Value3);
Assert.Equal(4.0, deserialized.Value4);
Assert.Equal(5L, deserialized.Value5);
}

[JsonSerializable(typeof(TypeWithNestedConverter<int, string>))]
[JsonSerializable(typeof(TypeWithSatisfiedConstraint<string>))]
[JsonSerializable(typeof(TypeWithDeeplyNestedConverter<int, string>))]
[JsonSerializable(typeof(TypeWithSingleLevelNestedConverter<int>))]
[JsonSerializable(typeof(TypeWithManyParams<int, string, bool, double, long>))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(long))]
internal partial class NestedGenericConverterContext : JsonSerializerContext
{
}
}
}
Loading
Loading