dotnet / dotnet/runtime

System.Text.Json source generation preserves unused constructors in Native AOT apps

Open
#132,839 4 comments 0 reactions 0 assignees View on GitHub
area-System.Text.Json linkable-framework
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

When a type is included in a System.Text.Json source-generated metadata context, Native AOT preserves all of the type's constructors, even when one constructor is explicitly selected with `[JsonConstructor]` and the other constructors cannot be used by serialization.

This can surface IL2026/IL3050 warnings from unreachable compatibility constructors and unnecessarily increase the native application size.

The generated deserializer correctly invokes only the attributed constructor. The additional constructors appear to be rooted by the generated `ConstructorAttributeProviderFactory`.

### Reproduction

`ConstructorRetention.csproj`:

```xml


Exe
net10.0
true
false



```

`Program.cs`:

```csharp
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text.Json.Serialization;

const string json = """
{
"PackageSourceRepository": "https://api.nuget.org/v3/index.json",
"ServiceIndex": "{}"
}
""";

var request = System.Text.Json.JsonSerializer.Deserialize(
json,
ReproJsonContext.Default.GetOperationClaimsRequest);

Console.WriteLine(request?.PackageSourceRepository);

public sealed class GetOperationClaimsRequest
{
public string? PackageSourceRepository { get; }

[JsonProperty("ServiceIndex")]
[JsonPropertyName("ServiceIndex")]
public string? ServiceIndexJson { get; }

[Newtonsoft.Json.JsonConstructor]
[System.Text.Json.Serialization.JsonConstructor]
public GetOperationClaimsRequest(
string? packageSourceRepository,
string? serviceIndexJson)
{
PackageSourceRepository = packageSourceRepository;
ServiceIndexJson = serviceIndexJson;
}

[Obsolete("Compatibility constructor")]
public GetOperationClaimsRequest(
string? packageSourceRepository,
JObject? serviceIndex)
: this(
packageSourceRepository,
serviceIndex?.ToString(
Formatting.None,
Array.Empty()))
{
}
}

[JsonSerializable(typeof(GetOperationClaimsRequest))]
internal partial class ReproJsonContext : JsonSerializerContext;
```

Publish with:

```console
dotnet clean -c Release -r win-x64
dotnet publish -c Release -r win-x64 -p:TrimmerSingleWarn=false
```

### Actual behavior

Native AOT reports warnings from the unused `(string, JObject)` constructor:

```text
warning IL2026: NuGet.Protocol.Plugins.GetOperationClaimsRequest.GetOperationClaimsRequest(String,JObject): Using member 'Newtonsoft.Json.Linq.JToken.ToString(Formatting,JsonConverter[])' which has 'RequiresUnreferencedCodeAttribute'...
warning IL3050: NuGet.Protocol.Plugins.GetOperationClaimsRequest.GetOperationClaimsRequest(String,JObject): Using member 'Newtonsoft.Json.Linq.JToken.ToString(Formatting,JsonConverter[])' which has 'RequiresDynamicCodeAttribute'...
```

The generated context correctly selects the two-string constructor:

```csharp
ObjectWithParameterizedConstructorCreator = static args =>
new GetOperationClaimsRequest((string)args[0], (string)args[1]);
```

However, it also emits:

```csharp
ConstructorAttributeProviderFactory = static () =>
typeof(GetOperationClaimsRequest).GetConstructor(
InstanceMemberBindingFlags,
binder: null,
new[] { typeof(string), typeof(string) },
modifiers: null);
```

`Type.GetConstructor(BindingFlags, Binder, Type[], ParameterModifier[])` is annotated with `DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors`. The Native AOT dependency graph consequently contains direct edges from dataflow analysis of the generated `Create_GetOperationClaimsRequest` method to both:

```text
Reflectable method: GetOperationClaimsRequest..ctor(string,string)
Reflectable method: GetOperationClaimsRequest..ctor(string,JObject)
```

Both constructor bodies are emitted in the Native AOT map, despite only the first being usable by System.Text.Json.

### Expected behavior

Only the constructor selected by `[JsonConstructor]` should be retained for source-generated deserialization. Unrelated constructors should be trimmed and should not produce trim/AOT warnings.

If constructor attribute metadata must remain available through `JsonTypeInfo.ConstructorAttributeProvider`, the generated rooting should preserve only the selected constructor rather than all public and non-public constructors.

### Impact

This occurs with NuGet.Protocol compatibility types such as `GetOperationClaimsRequest` and `GetServiceIndexResponse`. It causes warnings from obsolete constructors that are never invoked by System.Text.Json and retains their Newtonsoft.Json dependency paths in Native AOT applications.

`JsonSourceGenerationMode.Serialization` avoids emitting the constructor attribute-provider factory, but it cannot be used for deserialization, so it is not a workaround for this scenario. Warning suppressions also do not address the unnecessary native app size.

### Configuration

- .NET SDK 10.0.400
- .NET runtime / ILCompiler 10.0.11
- Windows `win-x64`

The constructor attribute-provider factory was introduced in #102902. I did not find existing Native AOT/trimming coverage for this constructor-retention behavior.

Contributor guide

Open the contributing guide

Research direction

Reproduce the issue with ConstructorRetention.csproj and Program.cs, then trace the generated ConstructorAttributeProviderFactory introduced in #102902. Run the documented Native AOT publish command and verify that only the [JsonConstructor] overload is retained, with no warnings from the unrelated constructor.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.