Boost .NET JSON Performance with System.Text.Json Source Generators
Learn how .NET 7+ source generators in System.Text.Json eliminate reflection, boost JSON serialization performance, and what trade‑offs to consider. A step‑by‑step example shows how to enable, build, and benchmark the generated code.
11 Oct 2025, 12:26 UTC

Why You’re Seeing Slower JSON in Your .NET App
Many developers hit a performance wall when serializing large or complex objects with System.Text.Json. The default runtime serializer uses reflection to walk the object graph, which can add 30–70% overhead compared to a hand‑written, strongly‑typed serializer. This overhead shows up as higher CPU usage and slower response times, especially in high‑throughput services.
Thesis: Compile‑Time Generation Removes Reflection Overhead
Starting with .NET 7, System.Text.Json ships a source generator that emits a dedicated serializer for each POCO type you annotate. The generated code is type‑safe, respects all serializer options, and runs at native speed—no reflection, no dynamic dispatch.
How Source Generators Fit Into Your Build
To enable a generator you only need three things:
- Target framework: .NET 7 or later.
- Annotate the target type with
[JsonSerializable(typeof(MyType))]. - Create a partial class that holds a static
JsonSerializerOptionsinstance.
The compiler then emits a MyType.g.cs file in the obj folder. That file contains a class that implements JsonSerializerContext and methods like WriteMyType() and ReadMyType().
Concrete Example
/// <summary>POCO to serialize</summary>
public class Order
{
public int Id { get; set; }
public string Customer { get; set; }
public List<OrderItem> Items { get; set; }
}
public class OrderItem
{
public string Product { get; set; }
public int Quantity { get; set; }
}
// 1. Tell the generator which types to build
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(OrderItem))]
public partial class JsonContext : JsonSerializerContext
{
// 2. Provide a reusable options instance
public static readonly JsonSerializerOptions Options = new JsonContext().Options;
}
Build the project (e.g., dotnet build). In the obj folder you’ll find JsonContext.g.cs containing the generated serializer.
Now serialize with:
var order = new Order { Id = 1, Customer = "Alice", Items = new List<OrderItem> { new OrderItem { Product = "Book", Quantity = 3 } } };
string json = JsonSerializer.Serialize(order, JsonContext.Options);
To benchmark, run a simple loop that serializes thousands of Order instances and compare the elapsed time to the default JsonSerializer.Serialize(order) call. You’ll typically see a 30–70% performance gain.
Trade‑Offs and Practical Limits
While source generators give you speed, they come with a few caveats:
- Compilation Time & Binary Size – Large object graphs generate more code, which can noticeably increase compile times and the final assembly size.
- No Dynamic Types – The generator must know every type at compile time. If your API accepts arbitrary JSON shapes, you’ll need the runtime serializer for those parts.
- Debugging Difficulty – Errors inside the generated code surface as compile‑time diagnostics, but stepping into the generated methods is not straightforward. Inspect the
objfile if you need to troubleshoot. - Rebuild Required on Model Changes – Adding a new property or type requires a full rebuild to regenerate the serializers.
How to Verify the Generated Code
After building, open obj/Debug/net8.0/JsonContext.g.cs and look for methods like WriteOrder(). Each property is written with a call to writer.WriteStringValue() or writer.WriteNumberValue()—no reflection calls. If you see a WriteObjectStart() followed by property writes, you’re looking at the right code.
For a sanity check, run a quick unit test that serializes an Order and asserts the JSON string matches an expected value. This ensures the generator is producing the correct output.
Actionable Take‑Aways
- Use source generators for performance‑critical paths where the object model is stable.
- Keep a separate
JsonContextper logical grouping of types to avoid bloating the generated file. - If you need dynamic deserialization, pair the generator with a fallback runtime serializer for those cases.
- Measure both compile time and runtime performance to confirm the trade‑off is worth it for your workload.
- Enable
dotnet build --verbosity diagto see the generator’s diagnostic output if you run into issues.
Conclusion
System.Text.Json source generators are a production‑ready feature that can shave significant CPU cycles off your JSON pipelines. By moving serialization logic to compile time, you eliminate reflection, gain type safety, and keep your codebase clean. Just be mindful of the compile‑time overhead and the static nature of the generated serializers. With careful planning, you can harness this feature to make your .NET services faster and more efficient.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.