System.Text.Json Source Generation: AOT-Ready Serialization Without Reflection

System.Text.Json works great out of the box. You call JsonSerializer.Serialize(myObject) and it figures everything out at runtime via reflection. But that runtime discovery has two costs:

  1. Slow first call while it builds metadata and converters
  2. Incompatible with Native AOT/trimming because the trimmer strips the metadata reflection needs

Source generation moves all of that to compile time.

The Problem

// Reflection-based: slow first call, breaks under trimming
string json = JsonSerializer.Serialize(product);

The Fix

Create a partial class extending JsonSerializerContext and annotate each type you need:

[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(Product[]))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonContext : JsonSerializerContext;

Use the generated type info instead of relying on reflection:

string json = JsonSerializer.Serialize(product, AppJsonContext.Default.Product);
var roundTripped = JsonSerializer.Deserialize(json, AppJsonContext.Default.Product);

AppJsonContext.Default.Product is the generated JsonTypeInfo<Product> - no reflection at any point.

Wiring into ASP.NET Core

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

Inserting at position 0 means source-generated metadata is checked first. Uncovered types fall back to reflection. For fully AOT-safe apps, set it as the sole resolver:

options.SerializerOptions.TypeInfoResolver = AppJsonContext.Default;

Now any unregistered type produces a clear error instead of a silent reflection fallback.

Polymorphism

Use [JsonDerivedType] on the base type:

[JsonDerivedType(typeof(ElectronicProduct), "electronic")]
[JsonDerivedType(typeof(ClothingProduct), "clothing")]
public record Product(int Id, string Name, decimal Price);
public record ElectronicProduct(int Id, string Name, decimal Price, string Warranty)
: Product(Id, Name, Price);

The serializer emits a $type discriminator and the source generator handles the rest.

Performance

Serializing an array of 100 products:

MethodFirst callSubsequent calls
Reflection-based~12 ms~45 us
Source-generated~0.3 ms~30 us

The big win is that first call: source generation eliminates the reflection warmup entirely. Subsequent calls are comparable or slightly faster, with roughly 15% fewer allocations on the fast path.

Key Takeaway

A partial class, a few [JsonSerializable] attributes, and you get instant first-call performance plus trim/AOT compatibility. If you’re targeting AOT, serverless, or just want faster startup, this is the path.