Properly Serialize Your Objects in .NET with Source Generators
Published on 13/02/2025
By  Christophe MOMMER

Serialization is an essential step when you want to expose objects in web programming. XML, JSON, and Protobuf are the three dominant formats, and the de facto standard these days is JSON. Therefore, this post will focus on that format.

As .NET has evolved, Microsoft realized that relying on a community package was probably not the best idea due to version compatibility issues. Hence, we saw the automatic integration of Newtonsoft.Json fade away to make room for the framework’s own serializer. While certain very specific use cases still require the community package, you can now accomplish a great deal with the framework’s serializer.

However, simply calling the JsonSerializer.Serialize method (and its inverse JsonSerializer.Deserialize) is not the most optimized approach. It’s likely the quickest thing to do, but Microsoft has given us built-in Source Generators within the framework to make these operations even faster.

First, a quick definition of a source generator. The concept isn’t new, but it’s now fully part of the framework and the compiler. Essentially, it’s a piece of code that analyzes elements of your code and, during the compilation process, emits additional code that becomes part of the final binary. In short, the compiler enhances your code.

Let’s consider a simple example: if I want to serialize an instance of a Person class that contains a first name and a last name, the simplest approach would be to produce raw text output where I manually write out the opening braces, the quotes, the labels, and the values. Well, that’s exactly what the framework’s source generator does for you!

Here’s the “standard” code:

public class Person
{
    public string Name { get; set; }
    public string LastName { get; set; }
}

var person = new Person { Name = "Christophe", LastName = "Mommer" };
var json = JsonSerializer.Serialize(person);

What the serializer does here is use the expensive process of reflection (runtime type introspection) to determine what needs to be serialized and how. This costly operation, although improved in the latest versions of .NET, still happens when you call Serialize.

We can help the process by generating code in advance, like this:

[JsonSerializable(typeof(Person))]
public partial class PersonSerializationContext : JsonSerializerContext { }

Several points make this class declaration interesting:

  1. It inherits from JsonSerializerContext, the class that defines the basic instructions for producing JSON serialization output (as described above, doing everything manually).
  2. While it’s entirely possible to write the code manually, we’re asking the compiler to do the heavy lifting before compiling the assembly. That’s why the class is marked as partial, allowing the compiler to create the “other part” of the class.
  3. Finally, adding the JsonSerializable attribute tells the source generator that this is the class where it should complete the code. Essentially, the source generator will scan your code to find all classes marked this way.

Once this is done, IDEs and the compiler are usually quite responsive, and you can find the automatically generated code in your project’s sources.

Lastly, you’ll need to change the call to the serialization method to use the generated source code instead of reflection:

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

Note: It’s possible to specify this serialization context in the JsonSerializerOptions class if needed, and you can also customize the generated code by using additional attributes.

This will give you the highest possible serialization performance, and best of all, the code is AOT-compatible.