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:
- A Windows, Linux, or Mac machine
- .NET 6/7 SDK (or newer)
- A .NET code editor or IDE (e.g. VS Code with the C# extension, Visual Studio, JetBrains Rider, or Fleet)
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"
}
{
"firstname": "Bill",
"lastname": "Bates",
"age": 22,
"role": "writer"
}
{
"firstname": "Caroline",
"lastname": "Collins",
"age": 33,
"role": "editor"
}
{
"firstname": "David",
"lastname": "Diggs",
"age": "44",
"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.