Skip to content

System.Text.Json source generator emits duplicate __GenericAccessors_ wrapper class for inaccessible members inherited from multiple generic base types #133423

Description

@svick

Description

The System.Text.Json source generator emits colliding helper classes for inaccessible members (e.g. a private [JsonInclude] property) that are inherited from two different generic base types of the same arity. The generated code then fails to compile with an error like CS0102: The type 'MyContext' already contains a definition for '__GenericAccessors_Leaf' (plus possibly a cascade of CS0264/CS0265/CS0699/CS0246).

This is a compilation failure that only happens in .NET 11; the same program compiles on .NET 10 (with a SYSLIB1038 warning), but then throws an InvalidOperationException when you attempt to use the generated serializer context.

Repro

Program.cs:

using System.Text.Json;
using System.Text.Json.Serialization;

Console.WriteLine(JsonSerializer.Serialize(new Leaf(), MyContext.Default.Leaf));

public class GrandBase<TG> where TG : class
{
    [JsonInclude]
    private TG GrandSecret { get; set; } = default!;
}

public class Base<TB> : GrandBase<string> where TB : struct
{
    [JsonInclude]
    private TB BaseSecret { get; set; }
}

public class Leaf : Base<int> { }

[JsonSerializable(typeof(Leaf))]
internal partial class MyContext : JsonSerializerContext { }

hwapp.csproj (multi-targeted to show the difference):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net10.0;net11.0</TargetFrameworks>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Behavior change between .NET 10 and .NET 11

Under .NET 10 the source generator does not emit accessors for inaccessible members: it reports SYSLIB1038 and the type throws InvalidOperationException at run time. Under .NET 11 the generator now emits [UnsafeAccessor] accessor wrapper classes for inaccessible members (added in #124650), but for members inherited from two different generic base types of the same arity the wrapper classes collide and the generated code no longer compiles.

> dotnet run -f net10.0
C:\code\tmp\hwapp\Program.cs(15,16): warning SYSLIB1038: The member 'Base.BaseSecret' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1038)
C:\code\tmp\hwapp\Program.cs(9,16): warning SYSLIB1038: The member 'GrandBase.GrandSecret' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1038)
Unhandled exception. System.InvalidOperationException: The property 'BaseSecret' on type 'Base`1[System.Int32]' which is annotated with 'JsonIncludeAttribute' is not accesible by the source generator.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnInaccessibleProperty(String memberName, Type declaringType)
   at System.Text.Json.Serialization.Metadata.JsonMetadataServices.PopulateProperties(JsonTypeInfo typeInfo, JsonPropertyInfoList propertyList, Func`2 propInitFunc)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.<get_PropertyList>g__CreatePropertyList|63_0()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.<EnsureConfigured>g__ConfigureSynchronized|174_0()
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType)
   at System.Text.Json.JsonSerializerOptions.GetTypeInfo(Type type)
   at MyContext.get_Leaf() in ...\MyContext.Leaf.g.cs:line 20
   at Program.<Main>$(String[] args) in C:\code\tmp\hwapp\Program.cs:line 4

> dotnet run -f net11.0
...\MyContext.Leaf.g.cs(121,26): error CS0102: The type 'MyContext' already contains a definition for '__GenericAccessors_Leaf'
...\MyContext.Leaf.g.cs(113,26): error CS0264: Partial declarations of 'MyContext.__GenericAccessors_Leaf<TB>' must have the same type parameter names in the same order
...\MyContext.Leaf.g.cs(121,60): error CS0699: 'MyContext.__GenericAccessors_Leaf<TB>' does not define type parameter 'TG'
...\MyContext.Leaf.g.cs(113,26): error CS0265: Partial declarations of 'MyContext.__GenericAccessors_Leaf<TB>' have inconsistent constraints for type parameter 'TB'
...\MyContext.Leaf.g.cs(124,74): error CS0246: The type or namespace name 'TG' could not be found (are you missing a using directive or an assembly reference?)
...\MyContext.Leaf.g.cs(124,30): error CS0246: The type or namespace name 'TG' could not be found (are you missing a using directive or an assembly reference?)
...\MyContext.Leaf.g.cs(126,76): error CS0246: The type or namespace name 'TG' could not be found (are you missing a using directive or an assembly reference?)
...\MyContext.Leaf.g.cs(126,85): error CS0246: The type or namespace name 'TG' could not be found (are you missing a using directive or an assembly reference?)

The build failed. Fix the build errors and run again.

(Reproduced on SDK 11.0.100-rc.2.26431.116; the net10.0 run used the .NET 10 GA shared framework.)

Root cause

The generator emits one member-accessor wrapper class per declaring type (keyed by property.DeclaringType.FullyQualifiedName), but names each one only after the constructed type:

// JsonSourceGenerator.Emitter.cs
writer.WriteLine($"private static class __GenericAccessors_{typeFriendlyName}<{typeParamList}>{constraintClauses}");

Here typeFriendlyName is the constructed type's TypeInfoPropertyName and typeParamList/constraintClauses come from the declaring type. When a type has inaccessible members inherited from two different generic base types of the same arity, both wrappers are emitted as __GenericAccessors_Leaf<...> with arity 1, so C# sees the same type defined twice.

Proposed fix

Include a declaring-type discriminator in the wrapper class name so each declaring type gets a distinct wrapper (analogous to how the per-member accessor methods are already disambiguated).

Notes

Note

This issue (including the reproduced output) was created with the help of GitHub Copilot.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions