Faster JSON with System.Text.Json source generation

ยท 1 min read

By default, System.Text.Json uses reflection to work out how to serialize your types. A source generator can do that work at compile time instead, for faster startup and trimming support.

The first time System.Text.Json serializes a type, it inspects it with reflection and builds metadata about its properties. That costs time at startup, on every type, and it's the reason reflection-based serialization doesn't work with trimming or Native AOT.

The source generator does the same work at compile time.

Set it up

Declare a partial context class listing the types you serialize:

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(OrderPlaced))]
[JsonSerializable(typeof(List<OrderSummary>))]
public partial class AppJsonContext : JsonSerializerContext
{
}

The compiler generates the serialization metadata for those types. Then pass it when you serialize:

var json = JsonSerializer.Serialize(message, AppJsonContext.Default.OrderPlaced);
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.OrderPlaced);

The overloads that take a JsonTypeInfo<T> are strongly typed, so there's no need for a generic argument either.

Use it in ASP.NET Core

Register the context so minimal APIs use it for request and response bodies:

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

Types that aren't in your context still fall back to the default reflection-based resolver, so you can add types gradually.

What you get

  • Faster startup. No reflection pass the first time each type is serialized. That's most noticeable in Azure Functions and other apps that start often.
  • Trimming and Native AOT support. Required if you publish trimmed or AOT-compiled apps, where reflection-based serialization doesn't work.
  • Compile-time feedback. Some unsupported type shapes show up as build warnings instead of runtime surprises.

When it isn't worth it

For a long-running web API where startup time doesn't matter, the gains are modest. And the context has to know every type up front, which doesn't fit code that serializes arbitrary types at runtime, such as a generic object payload. Use it where startup, trimming or AOT matter, and don't feel you have to convert everything else.

Takeaway

Add a JsonSerializerContext for the types you serialize most, pass its type info when you serialize, and register it with ASP.NET Core. You get faster startup and support for trimming and Native AOT, and you can adopt it one type at a time.