· 4 min read

Using Source Generators for JSON Serialization in .NET

.NET 6 introduced a source generator for System.Text.Json. This post explores how to use it to increase JSON serialization performance significantly.

.NET 6 introduced a source generator for System.Text.Json. This post explores how to use it to increase JSON serialization performance significantly.

System.Text.Json is first introduced in .NET Core 3.0 as a lightweight, high-performance JSON library, shipped as part of the .NET Core SDK. It is designed to be a better-fit for ASP.NET Core applications, which previously relied on Newtonsoft.Json. System.Text.Json focuses more on performance and simplicity and it’s only getting faster with every .NET release.

Traditionally, JSON serialization in .NET is done using reflection. The JsonSerializer class uses reflection to collect metadata of the data types at runtime. Although it caches the metadata for subsequent use, the collection process takes time and uses memory. Reflection is a powerful feature, but it comes with a performance cost. So an alternate solution is provided in .NET 6 using source generators.

Source Generators

In .NET 5, Microsoft introduced a new feature called Source Generators. Source Generators let you generate C# source code on the fly during compilation. This generated code is then compiled into your assembly and can be used by your application. Source generators provides following benefits:

  • Improved performance
  • Reduced memory usage
  • Allow Assembly Trimming

In .NET 6, Microsoft introduced a source generator for System.Text.Json. This source generator generates serialization code for your types at compile time. This generated code can be used with JsonSerializer class to serialize and deserialize JSON. This approach is significantly faster than using reflection.

Using the JSON Source Generator

Say we have a class called Book as follows:

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public int Pages { get; set; }
}

Typically, one would use JsonSerializer class to serialize and deserialize JSON as follows:

// Serialize
var json = JsonSerializer.Serialize(book);

// Deserialize
var book = (Book)JsonSerializer.Deserialize(json, typeof(Book));

There are lots of overloads available for the JsonSerializer class to do the same. But all of them use reflection to collect metadata of the Book class at runtime.

To use the JSON source generator, create a partial internal class that inherits JsonSerializerContext. Ensure that the class must be internal and partial and should be present in the same assembly as the types you want to serialize (In this case, the type Book). This class is used by the source generator to generate serialization code for your types.

internal partial class MyJsonSerializerContext : JsonSerializerContext
{
}

Now to mark the Book class for serialization, add the JsonSerializable attribute to it as follows:

[JsonSerializable(typeof(Book))]
internal partial class MyJsonSerializerContext : JsonSerializerContext
{
}

That’s it! Now when you build your project, the source generator will generate serialization code for the Book class on the fly. To make use of this generated code, use a JsonSerializer method that takes one of the following parameters:

  • JsonSerializerContext instance
  • JsonTypeInfo<T> instance
  • JsonSerializerOptions instance with its TypeResolver property set to Default property of the context type (In this case, MyJsonSerializerContext.Default)

Note that the last one is supported only in .NET 7 and above.

The following snippet shows how to serialize JSON with the JsonSerializerContext instance:

// Serialize
var json = JsonSerializer.Serialize(book, MyJsonSerializerContext.Default);
// OR
var json = JsonSerializer.Serialize(book, MyJsonSerializerContext.Default.Book);
// OR
var options = new JsonSerializerOptions
{
    TypeResolver = MyJsonSerializerContext.Default
};
var json = JsonSerializer.Serialize<Book>(book, options);

The following snippet shows how to deserialize JSON with the JsonSerializerContext instance:

// Deserialize
var book = (Book)JsonSerializer.Deserialize(json, typeof(Book), MyJsonSerializerContext.Default);
// OR
var book = (Book)JsonSerializer.Deserialize(json, MyJsonSerializerContext.Default.Book);
// OR
var options = new JsonSerializerOptions
{
    TypeResolver = MyJsonSerializerContext.Default
};
var book = (Book)JsonSerializer.Deserialize<Book>(json, typeof(Book), options);

To add more types for serialization, add the JsonSerializable attribute to the MyJsonSerializerContext class as follows:

[JsonSerializable(typeof(Book))]
[JsonSerializable(typeof(Author))]
[JsonSerializable(typeof(Publisher))]
internal partial class MyJsonSerializerContext : JsonSerializerContext
{
}

Wrapping Up

In this post, we saw how to use the JSON source generator to serialize and deserialize JSON in .NET. This approach is significantly much more performant compared to using reflection, although there are some caveats as it isn’t feature-complete compared to reflection mode. You can also refer to the official documentation for more information. Besides this approach also makes it easy to transition your application to Native AOT compilation in .NET 8 and later, as it is reflection-free. So the next time you need to squeeze out every bit of performance from Json serialization or planning out for NativeAOT, consider using the JSON source generator. Happy coding!

Back to Blog

Related Posts

View All Posts »
Writing Tests for Dapper with TestContainers in xUnit

Writing Tests for Dapper with TestContainers in xUnit

Writing tests for Dapper is traditionally challenging due to two reasons. In this post, we will explore how to write integration test fixtures for Dapper queries using TestContainers in xUnit and how to reuse the database container across multiple tests.