Building applications is a virtuous circle of learning about problems, finding solutions, and optimizing. I find optimizing an application the most fun, as it can help you squeeze performance out from surprising places.

In this post, we’ll see how you can use the latest JSON source generators shipped in .NET 6 to improve your JSON API performance and increase your response throughput.

Like many source generators in .NET, the System.Text.Json source generator enhances an existing partial class with essential elements required for serialization. Those elements include:

  • A JsonTypeInfo<T> for each serializable entity in your object graph.
  • A default instance of a JsonSerializerContext.
  • JsonSerialiazerOptions for formatting JSON on serialization.

Don’t worry; it’s not as complex as it sounds. Let’s first start with our entity.

public record Person(
    string Name,
    bool IsCool = true,
    Person? Friend = null
);

We want to optimize the serialization of this record. Since we know structurally what this entity looks like, we can create a rigid and streamlined serializer, which should improve overall performance. Therefore, we first define a PersonSerializationContext derived from JsonSerialzerContext.

[JsonSourceGenerationOptions(
    WriteIndented = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Person))]
public partial class PersonSerializationContext 
    : JsonSerializerContext {}

Essential elements of this definition include:

The partial keyword. The source generator will create the implementation of our class for us. The JsonSourceGenerationOptions attribute allows us to customize serialization with the correct casing, null handling, and much more. Don’t forget this if you have specific serialization needs. The JsonSerializable attribute for non-standard types during serialization. Each attribute of this kind will produce a JsonTypeInfo property on our context class.

Cool, let’s see how we can use our new PersonSerializationContext.

using System.Text.Json;
using System.Text.Json.Serialization;

var person = new Person("Khalid", Friend: new("Maarten"));

var json =
    JsonSerializer.Serialize(
        person,
        PersonSerializationContext.Default.Person);

Console.WriteLine(json);

Running our application, we see the desired output. You’ll note that the result uses camel casing and that serialization did not output null values.

{
  "name": "Khalid",
  "isCool": true,
  "friend": {
    "name": "Maarten",
    "isCool": true
  }
}

So, how do you take advantage of your new PersonSerializationContext in an ASP.NET Core application? Well, it’s pretty straightforward.

Inside of an existing Minimal APIs endpoint, you’ll need to use the Results.Json method and pass it your generated Options found on the Default property.

using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("person", () => 
    Results.Json(
        new Person("Khalid", Friend: new("Maarten")), 
        PersonSerializationContext.Default.Options));

app.Run();

[JsonSourceGenerationOptions(
    WriteIndented = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Person))]
public partial class PersonSerializationContext 
    : JsonSerializerContext {}

public record Person(
    string Name,
    bool IsCool = true,
    Person? Friend = null
);

Running the ASP.NET Core application and hitting the endpoint produces the expected results, the same as we saw before in our console application.

// 20230210114342
// http://localhost:5115/person

{
  "name": "Khalid",
  "isCool": true,
  "friend": {
    "name": "Maarten",
    "isCool": true
  }
}

Conclusion

While hearing the phrase “source generators” might be scary at first, this optimization can improve your APIs’ performance with minimal effort. A few lines of code and some refactoring, and you’re off to the races.

If you’re going to use this technique, I recommend finding your application’s hottest paths and starting there. Then measure to see what kind of serialization overhead you’ve reduced.

As always, thanks for reading and sharing my posts with your colleagues. If you have any questions, please feel free to reach out.