Source-generated fast-path serialization bypasses TypeInfoResolverChain for object-typed values
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
When using `JsonSerializerOptions.TypeInfoResolverChain` to combine a source-generated `JsonSerializerContext` with `DefaultJsonTypeInfoResolver` as a fallback, the fallback resolver is never consulted for types encountered during serialization of types known to the source-generated context. This is because the `JsonTypeInfo` returned by the source-generated context carries a fast-path serialize handler (`SerializeHandler`) that writes directly via `Utf8JsonWriter`, resolving types only through the source-generated context's own options -- completely bypassing the resolver chain.
This makes it impossible to use `TypeInfoResolverChain` as a fallback mechanism for types that appear at runtime inside `object`-typed properties (e.g., `IDictionary` values).
This is related to, but distinct from, #71933 (fixed in .NET 8). That issue was about the fast-path not activating at all in combined contexts. This issue is that the fast-path *does* activate but resolves types exclusively through the originating context, ignoring other resolvers in the chain.
### Reproduction Steps
```csharp
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
var options = new JsonSerializerOptions();
options.TypeInfoResolverChain.Add(ExampleContext.Default);
options.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver());
var data = new ExampleData
{
Metadata = new Dictionary
{
["timestamp"] = DateTime.UtcNow,
["name"] = "test"
}
};
// Throws NotSupportedException for System.DateTime
string json = JsonSerializer.Serialize(data, options);
Console.WriteLine(json);
public class ExampleData
{
public IDictionary Metadata { get; set; } = new Dictionary();
}
[JsonSourceGenerationOptions]
[JsonSerializable(typeof(ExampleData))]
internal partial class ExampleContext : JsonSerializerContext;
```
### Expected behavior
Serialization succeeds. The source-generated context handles `ExampleData` and `IDictionary`, and the `DefaultJsonTypeInfoResolver` (second in the chain) handles `DateTime` and any other runtime types encountered in `object`-typed values. This is the documented purpose of `TypeInfoResolverChain` -- to allow fallback to additional resolvers.
### Actual behavior
```
System.NotSupportedException: JsonTypeInfo metadata for type 'System.DateTime' was not provided
by TypeInfoResolver of type 'ExampleContext'. If using source generation, ensure that all root
types passed to the serializer have been annotated with 'JsonSerializableAttribute', along with
any types that might be serialized polymorphically.
The unsupported member type is located on type 'System.Object'. Path: $.
```
The error message references `ExampleContext` (the source-generated context alone), not the combined resolver chain, confirming that the fast-path handler resolves types exclusively through the source-generated context.
Stack trace:
```
at System.Text.Json.ThrowHelper.ThrowNotSupportedException(WriteStack& state, Exception innerException)
at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Serialize(Utf8JsonWriter writer, T& rootValue, Object rootValueBoxed)
at System.Text.Json.JsonSerializer.Serialize[TValue](Utf8JsonWriter writer, TValue value, JsonTypeInfo`1 jsonTypeInfo)
at ExampleContext.ExampleDataSerializeHandler(Utf8JsonWriter writer, ExampleData value)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.d__15.MoveNext()
```
### Regression?
Unknown. The fast-path serialize handler has been present since the source generator was introduced. `TypeInfoResolverChain` was introduced in .NET 8. The `OriginatingResolver`-based fast-path compatibility logic was added in .NET 8 via #71933 / PR #80741.
### Known Workarounds
Set `GenerationMode = JsonSourceGenerationMode.Metadata` on the `[JsonSourceGenerationOptions]` attribute to suppress generation of fast-path serialize handlers:
```csharp
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(ExampleData))]
internal partial class ExampleContext : JsonSerializerContext;
```
Without the fast-path handler, serialization goes through the normal converter pipeline, which uses the `JsonSerializerOptions` and its `TypeInfoResolverChain` to resolve all types -- including the fallback to `DefaultJsonTypeInfoResolver` for types not known to the source-generated context.
### Configuration
- .NET 10.0 (SDK 10.0.103), but likely affects .NET 8 and .NET 9 as well
- Windows 11 x64
- Not specific to OS or architecture
### Other information
The root cause is in how `CanUseSerializeHandler` is determined in `JsonTypeInfo.Configure()`:
```csharp
CanUseSerializeHandler = HasSerializeHandler && IsCompatibleWithCurrentOptions;
```
Where `IsCompatibleWithCurrentOptions` is determined by `DetermineIsCompatibleWithCurrentOptions()`, which checks `OriginatingResolver.IsCompatibleWithOptions(Options)`. When the `JsonTypeInfo` originates from a source-generated context, both `OriginatingResolver` and `Options` belong to that context, so the fast-path is deemed compatible. The problem is that this check doesn't account for the fact that the `JsonTypeInfo` may be used through a `TypeInfoResolverChain` where additional resolvers are needed for types discovered at runtime.
A possible fix would be to disable the fast-path serialize handler when the `JsonTypeInfo` is being consumed through options whose `TypeInfoResolver` differs from the `OriginatingResolver` -- i.e., when the type info was resolved through a chain rather than directly from its originating context.
Contributor guide
Assessment
This issue has not been assessed yet.