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:
- Slow first call while it builds metadata and converters
- 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 trimmingstring 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:
| Method | First call | Subsequent 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.