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:
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
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
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:
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]:
[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:
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:
[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:
<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:
- A
JsonTypeInfo<T>that describes the type's shape (properties, types, attributes). - Property getter and setter delegates (avoiding reflection).
- A
JsonConverter<T>with optimisedReadandWritemethods.
You can inspect the generated code by adding <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> to your project.
Key Takeaways
- Use
[JsonSerializable]on a partialJsonSerializerContextfor each type you serialise. - Pass the generated context explicitly for maximum clarity and safety.
- Set
[JsonSourceGenerationOptions]for default configuration like camelCase naming. - The source generator is required for Native AOT — reflection-based serialisation will not survive trimming.
- Remember to register collection types (e.g.,
List<T>) separately from their element types.