The JSON Source Generator in System.Text.Json

By default, System.Text.Json uses reflection to inspect types at runtime, discovering properties, checking attributes, and building serialisation metadata on the fly. This works well enough, but reflection has costs: slower startup, higher memory usage, and incompatibility with Native AOT. The JSON source generator solves all three by generating serialisation code at compile time.

The Problem with Reflection

When you call JsonSerializer.Serialize(myObject), the serialiser must figure out what properties myObject has, what their types are, how to read their values, and whether any [JsonPropertyName] or [JsonIgnore] attributes are applied. It does this through reflection, and it caches the result — but the first call for each type is expensive.

More critically, reflection-based serialisation is incompatible with Native AOT trimming. The trimmer cannot statically determine which types will be serialised, so it may remove the very metadata the serialiser needs.

Setting Up the Source Generator

Create a JsonSerializerContext class and decorate it with [JsonSerializable] for each type you want to serialise:

Example.cs
using System.Text.Json;
using System.Text.Json.Serialization;

public class WeatherForecast
{
    public DateOnly Date { get; set; }
    public int TemperatureC { get; set; }
    public string? Summary { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

[JsonSerializable(typeof(WeatherForecast))]
[JsonSerializable(typeof(List<WeatherForecast>))]
public partial class AppJsonContext : JsonSerializerContext
{
}

The class must be partial — the source generator fills in the implementation. Each [JsonSerializable] attribute tells the generator to produce metadata and serialisation logic for that specific type.

Using the Generated Context

There are three ways to use the generated context:

Explicit Context

Example.cs
var json = JsonSerializer.Serialize(
    forecast,
    AppJsonContext.Default.WeatherForecast);

var obj = JsonSerializer.Deserialize(
    json,
    AppJsonContext.Default.WeatherForecast);

This is the most explicit approach. You pass the generated JsonTypeInfo<T> directly, leaving no ambiguity about which metadata is used.

Via JsonSerializerOptions

Example.cs
var options = new JsonSerializerOptions
{
    TypeInfoResolver = AppJsonContext.Default
};

var json = JsonSerializer.Serialize(forecast, options);
var obj = JsonSerializer.Deserialize<WeatherForecast>(json, options);

This plugs the generated context into the options object. Any type registered with [JsonSerializable] will use generated metadata; unregistered types will throw at runtime.

As Default for ASP.NET Core

In ASP.NET Core, configure it in Program.cs:

Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain
        .Insert(0, AppJsonContext.Default);
});

// Or for controllers:
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.TypeInfoResolverChain
            .Insert(0, AppJsonContext.Default);
    });

Customising Serialisation

You can apply JsonSerializerOptions defaults to the context using [JsonSourceGenerationOptions]:

Example.cs
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(WeatherForecast))]
public partial class AppJsonContext : JsonSerializerContext
{
}

Per-property customisation works with the standard attributes:

Example.cs
public class WeatherForecast
{
    [JsonPropertyName("date")]
    public DateOnly Date { get; set; }

    [JsonIgnore]
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

The source generator reads these attributes at compile time and bakes them into the generated code.

Generation Modes

The [JsonSerializable] attribute accepts a GenerationMode parameter:

Example.cs
[JsonSerializable(typeof(WeatherForecast),
    GenerationMode = JsonSourceGenerationMode.Serialization)]
Mode What It Generates
Default Both metadata and optimised serialisation/deserialisation
Metadata Type metadata only — still uses the standard converters at runtime
Serialization Optimised serialise/deserialise methods

Default gives you the best performance. Metadata is useful when you only need AOT compatibility but do not need the performance gain of custom serialisation logic.

AOT Compatibility

The source generator is essential for Native AOT. Without it, JsonSerializer relies on reflection that gets trimmed away. With the source generator, all metadata is baked into the compiled binary.

To verify your application is AOT-compatible, enable trimming warnings:

config.xml
<PropertyGroup>
    <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

If you call JsonSerializer.Serialize<T>() without a context and trimming is enabled, you will get warning IL2026 or IL3050 pointing you to the source generator.

What Gets Generated

The generator produces several things per type:

You can inspect the generated code by adding <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> to your project.

Key Takeaways