.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
instanceJsonTypeInfo<T>
instanceJsonSerializerOptions
instance with itsTypeResolver
property set toDefault
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!