Skip to content

Source-generated fast-path serialization bypasses TypeInfoResolverChain for object-typed values #125375

@bart-vmware

Description

@bart-vmware

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<string, object> 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

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<string, object>
    {
        ["timestamp"] = DateTime.UtcNow,
        ["name"] = "test"
    }
};

// Throws NotSupportedException for System.DateTime
string json = JsonSerializer.Serialize(data, options);
Console.WriteLine(json);

public class ExampleData
{
    public IDictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
}

[JsonSourceGenerationOptions]
[JsonSerializable(typeof(ExampleData))]
internal partial class ExampleContext : JsonSerializerContext;

Expected behavior

Serialization succeeds. The source-generated context handles ExampleData and IDictionary<string, object>, 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.<SerializeAsync>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:

[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():

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions