System.Text.Json source generation preserves unused constructors in Native AOT apps
- 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
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