How to use JsonSerializerOptions with source generated JSON deserialization

March 29, 2023
Written by
Bryan Hogan
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Use JsonSerializerOptions with source generated JSON deserialization

In 2021, Microsoft released a source generator that improves the performance of serialization with the System.Text.Json APIs.

It is generally easy to use, you add an annotation to the class you want to serialize/deserialize, and then use the generated type when performing the serialization/deserialization.

But it is not immediately obvious how to use JsonSerializerOptions when deserializing.

There are a number of reasons to use JsonSerializerOptions when deserializing. For example, if the C# properties don't match the case of the JSON keys, or you need to map an enum to a string. In these cases, you could use attributes on the class you are deserializing, but I find these annotations distracting when reading the code.

Prerequisites

You will need the following things in this tutorial:

The problem

If you have the following JSON -

person1.json:

{
    "firstname": "Alice",
    "lastname": "Adams",
    "age": 11,
    "role": "writer"
}

And use the following code to deserialize it:

string json1 = File.ReadAllText("person1.json");
Person person1 = JsonSerializer.Deserialize(json1, MyJsonContext.Default.Person);
Console.WriteLine($"Person1: {person1}");

The output will be -

Person1:   (0), is 0 years old.

Not what you want.

The JSON key names are not exact matches for the property names, and the role is a string, rather than a number (the backing type of an enum).

The fix is to use JsonSerializerOptions to control the deserialization process.

The solution

Create new console application:

dotnet new console -n JsonSerializerOptionsSourceGenerated
cd JsonSerializerOptionsSourceGenerated

Open the .csproj file and add the following ItemGroup -

<ItemGroup>
  <None Update="*.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup> 

This will copy the JSON files to the output directory so that they can be read by the console app.

The JSON files

Create four JSON files:

{
    "firstname": "Alice",
    "lastname": "Adams",
    "age": 11,
    "role": "reader"
}

Note that the key names don't match the name of the properties in the Person class, Role is a string, and that person4.json has a string for the age. This is deliberate, to illustrate how options work with the source generated deserialization code.

The type to deserialize

Create a new file called Person.cs. It has strings for first and last names, an int for age, and an enum for role. The age and role are deliberately not strings, to illustrate features/deficiencies of the source generated deserialization code.

Lines 3-4 are the annotations that tell the source generator to generate the code to serialize/deserialize the Person class.

using System.Text.Json.Serialization;

[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext {}

public class Person
{
    public override string ToString()
    {
        return $"{Firstname} {Lastname} ({Role}), is {Age} years old.";
    }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public int Age { get; set; }
    public Role Role { get; set;}
}

public enum Role
{
    Reader = 1,
    Writer = 2,
    Editor = 3
}

Performing the deserialization

In the Program.cs file add -

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

string json1 = File.ReadAllText("person1.json");
Person person1 = JsonSerializer.Deserialize(json1, MyJsonContext.Default.Person);
Console.WriteLine($"Person1: {person1}");

At this point, you can run the application.

The output will be -

Person1:   (0), is 0 years old.

Not what you want!

The fix is to use JsonSerializerOptions to control the deserialization process.

Add the following code to Program.cs:

var options = new JsonSerializerOptions
{
    Converters = 
    {
         new JsonStringEnumConverter() 
    },
    NumberHandling = JsonNumberHandling.AllowReadingFromString, // this won't work
    PropertyNameCaseInsensitive = true,
    TypeInfoResolver = MyJsonContext.Default
};

Deserialization approach 1

You can try again to deserialize the json1 string. Pass the options object to the Deserialize method:

person1 = JsonSerializer.Deserialize<Person>(json1, options);
Console.WriteLine($"Person1(using JsonSerializerOptions): {person1}");


The output will be:

Person1(using JsonSerializerOptions): Alice Adams (Reader), is 11 years old.

Deserialization approach 2

Create an instance of MyJsonContext and pass in the options you created above:

MyJsonContext myJsonContext = new MyJsonContext(options);

Now you can deserialize the JSON using the Deserialize overload that takes JsonTypeInfo<TValue>.

string json2 = File.ReadAllText("person2.json");
Person person2 = JsonSerializer.Deserialize(json2, myJsonContext.Person);
Console.WriteLine($"Person2: {person2}");

The output will be:

Person2: Bill Bates (Writer), is 22 years old.

Deserialization approach 3

Another way to deserialize is to use the Deserialize overload that takes a Type and JsonSerializerOptions.

string json3 = File.ReadAllText("person3.json");
Person person3 = JsonSerializer.Deserialize(json3, typeof(Person), options) as Person;

Console.WriteLine($"Person3: {person3}");

The output will be:

Person3: Caroline Collins (Editor), is 33 years old.

The problem with JsonNumberHandling.AllowReadingFromString

When using source generated code for deserialization, the JsonNumberHandling.AllowReadingFromString option is not supported.

The file person4.json has a string for the age - "age": "44". This will throw an exception when deserialized.

string json4 = File.ReadAllText("person4.json");
try
{
    Person person4 = JsonSerializer.Deserialize(json4, myJsonContext.Person); // number handling option is ignored
    Console.WriteLine($"Person4: {person4}");
}
catch (Exception ex)
{
    Console.WriteLine($"Person4: {ex.Message}");
}

The output from this will be:

Person4: The JSON value could not be converted to System.Int32. Path: $.age | LineNumber: 3 | BytePositionInLine: 15.

If you need support for handling numbers read from strings, source generated deserialization will not work for you.

Conclusion

In this post, you learned how to use JsonSerializerOptions with source generated deserialization code. But you should keep in mind that not all the familiar options are not available when using source generated code.

Bryan Hogan is a blogger, podcaster, Microsoft MVP, and Pluralsight author. He has been working on .NET for almost 20 years. You can reach him on Twitter @bryanjhogan.