Preface
Json is a popular data format for data exchange between systems. And it’s a common task to serialize and deserialize json data in .NET applications. In this post, I will try some enhancements in .NET for json serialization and deserialization.
Packages
In .NET Core 3.0, Microsoft introduced a new json library System.Text.Json
to replace Newtonsoft.Json
. The new library is faster and has better performance than the old one.
The features of System.Text.Json
are not as rich as Newtonsoft.Json
, but it’s enough for most scenarios. And System.Text.Json
is updated frequently, and new features are added in each release,
you can see the table for the differences between System.Text.Json
and Newtonsoft.Json
here.
Reflection
Reflection is a powerful feature in .NET. And it’s used in json serialization and deserialization. In the new library System.Text.Json
, reflection is used to get the properties of a class, and then serialize or deserialize the object.
using System;
using System.Reflection;
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime CreatedAt { get; set; }
public List<string> Tags { get; set; }
public Dictionary<string, int> Scores { get; set; }
}
public class Program
{
public static void Main()
{
var myClass = new MyClass{
Id = 1,
Name = "Hello",
CreatedAt = DateTime.Now,
Tags = new List<string>{"tag1", "tag2"},
Scores = new Dictionary<string, int>{{"score1", 100}, {"score2", 200}}
};
var type = myClass.GetType();
var properties = type.GetProperties();
foreach (var property in properties)
{
Console.WriteLine($"Property: {property.Name}, Type: {property.PropertyType}, Value: {property.GetValue(myClass)}");
}
}
}
// output:
// Property: Id, Type: System.Int32, Value: 1
// Property: Name, Type: System.String, Value: Hello
// Property: CreatedAt, Type: System.DateTime, Value: 7/22/2024 8:15:19 PM
// Property: Tags, Type: System.Collections.Generic.List`1[System.String], Value: System.Collections.Generic.List`1[System.String]
// Property: Scores, Type: System.Collections.Generic.Dictionary`2[System.String,System.Int32], Value: System.Collections.Generic.Dictionary`2[System.String,System.Int32]
The following code shows how to get the properties of a class using reflection
.
This is a very convenient feature, but it’s very slow.
JsonPropertyName
To reduce the reflection usage, I guess using JsonPropertyName
is a good choice.
JsonPropertyName
is an attribute in System.Text.Json
to specify the property name in json, so it won’t use reflection to get the property name and try many type of naming strategy.
public class MyClassWithPropertyName
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; }
[JsonPropertyName("scores")]
public Dictionary<string, int> Scores { get; set; }
}
SourceGeneration
To reduce the reflection usage is to use SourceGeneration
.
SourceGeneration
is a new feature in .NET 5, and it’s used to generate source code at compile time.
It will size up the build package, but it reduce huge amount of reflection usage in runtime.
public class MyClassWithSourceGenerators
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime CreatedAt { get; set; }
public List<string> Tags { get; set; }
public Dictionary<string, int> Scores { get; set; }
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(List<MyClassWithSourceGenerators>))]
public partial class SourceGeneratorsContext: JsonSerializerContext
{
}
Benchmark
Using BenchmarkDotNet
to test the performance of System.Text.Json
and Newtonsoft.Json
.
using System.Text.Json;
using BenchmarkDotNet.Attributes;
[ShortRunJob]
[HtmlExporter]
public class BenchmarkBase
{
protected List<MyClass> objects;
protected string json;
protected List<MyClassWithPropertyName> objectsWithPropertyName;
protected string jsonWithPropertyName;
protected List<MyClassWithSourceGenerators> objectsWithSourceGenerators;
protected string jsonWithSourceGenerators;
protected JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false };
[Params(1, 1000, 1000000)]
public int n = 0;
[GlobalSetup]
public void Setup()
{
objects = Enumerable.Range(0, n).Select(i => new MyClass
{
Id = i,
Name = $"Object {i}",
CreatedAt = DateTime.Now,
Tags = new List<string> { "Tag1", "Tag2", "Tag3" },
Scores = new Dictionary<string, int> { { "Score1", i }, { "Score2", i * 2 } }
}).ToList();
json = JsonSerializer.Serialize(objects, options);
objectsWithPropertyName = Enumerable.Range(0, n).Select(i => new MyClassWithPropertyName
{
Id = i,
Name = $"Object {i}",
CreatedAt = DateTime.Now,
Tags = new List<string> { "Tag1", "Tag2", "Tag3" },
Scores = new Dictionary<string, int> { { "Score1", i }, { "Score2", i * 2 } }
}).ToList();
jsonWithPropertyName = JsonSerializer.Serialize(objectsWithPropertyName, options);
objectsWithSourceGenerators = Enumerable.Range(0, n).Select(i => new MyClassWithSourceGenerators
{
Id = i,
Name = $"Object {i}",
CreatedAt = DateTime.Now,
Tags = new List<string> { "Tag1", "Tag2", "Tag3" },
Scores = new Dictionary<string, int> { { "Score1", i }, { "Score2", i * 2 } }
}).ToList();
jsonWithSourceGenerators = JsonSerializer.Serialize(objectsWithSourceGenerators, SourceGeneratorsContext.Default.ListMyClassWithSourceGenerators);
}
}
[MemoryDiagnoser]
public class SerializeBenchmark : BenchmarkBase
{
[Benchmark(Baseline = true)]
public string SerializeRegular() => JsonSerializer.Serialize(objects, options);
[Benchmark]
public string SerializePropertyName() => JsonSerializer.Serialize(objectsWithPropertyName, options);
[Benchmark]
public string SerializeSourceGen() => JsonSerializer.Serialize(objectsWithSourceGenerators, SourceGeneratorsContext.Default.ListMyClassWithSourceGenerators);
}
[MemoryDiagnoser]
public class DeserializeBenchmark : BenchmarkBase
{
[Benchmark(Baseline = true)]
public List<MyClass> DeserializeRegular() => JsonSerializer.Deserialize<List<MyClass>>(json, options);
[Benchmark]
public List<MyClassWithPropertyName> DeserializePropertyName() => JsonSerializer.Deserialize<List<MyClassWithPropertyName>>(jsonWithPropertyName, options);
[Benchmark]
public List<MyClassWithSourceGenerators> DeserializeSourceGen() => JsonSerializer.Deserialize(jsonWithSourceGenerators, SourceGeneratorsContext.Default.ListMyClassWithSourceGenerators);
}
Result
// * Summary *
BenchmarkDotNet v0.13.12, Ubuntu 22.04.3 LTS (Jammy Jellyfish) WSL
AMD Ryzen 7 1700, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.303
[Host] : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
ShortRun : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
| Method | n | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|---------------------- |-------- |-------------------:|-------------------:|-----------------:|------:|--------:|--------:|--------:|--------:|------------:|------------:|
| SerializeRegular | 1 | 1,286.0 ns | 282.2 ns | 15.47 ns | 1.00 | 0.00 | 0.0725 | - | - | 616 B | 1.00 |
| SerializePropertyName | 1 | 1,295.2 ns | 210.2 ns | 11.52 ns | 1.01 | 0.00 | 0.0725 | - | - | 616 B | 1.00 |
| SerializeSourceGen | 1 | 862.9 ns | 1,050.0 ns | 57.56 ns | 0.67 | 0.04 | 0.0362 | - | - | 304 B | 0.49 |
| | | | | | | | | | | | |
| SerializeRegular | 1000 | 1,131,925.1 ns | 506,352.4 ns | 27,754.88 ns | 1.00 | 0.00 | 76.1719 | 76.1719 | 76.1719 | 292975 B | 1.00 |
| SerializePropertyName | 1000 | 1,129,028.9 ns | 668,265.2 ns | 36,629.87 ns | 1.00 | 0.06 | 76.1719 | 76.1719 | 76.1719 | 294954 B | 1.01 |
| SerializeSourceGen | 1000 | 748,664.7 ns | 310,590.2 ns | 17,024.50 ns | 0.66 | 0.03 | 72.2656 | 72.2656 | 72.2656 | 292078 B | 1.00 |
| | | | | | | | | | | | |
| SerializeRegular | 1000000 | 1,323,098,391.7 ns | 1,189,546,821.8 ns | 65,203,075.25 ns | 1.00 | 0.00 | - | - | - | 316000824 B | 1.00 |
| SerializePropertyName | 1000000 | 1,133,749,583.0 ns | 1,132,105,019.4 ns | 62,054,496.23 ns | 0.86 | 0.07 | - | - | - | 318001096 B | 1.01 |
| SerializeSourceGen | 1000000 | 775,491,397.3 ns | 188,378,076.0 ns | 10,325,638.00 ns | 0.59 | 0.03 | - | - | - | 316000152 B | 1.00 |
| Method | n | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|------------------------ |-------- |-----------------:|------------------:|---------------:|------:|--------:|-----------:|-----------:|-------------:|------------:|
| DeserializeRegular | 1 | 2.401 μs | 0.8486 μs | 0.0465 μs | 1.00 | 0.00 | 0.1297 | - | 1.08 KB | 1.00 |
| DeserializePropertyName | 1 | 2.742 μs | 3.8059 μs | 0.2086 μs | 1.14 | 0.11 | 0.1297 | - | 1.08 KB | 1.00 |
| DeserializeSourceGen | 1 | 2.471 μs | 6.4010 μs | 0.3509 μs | 1.03 | 0.13 | 0.1297 | - | 1.08 KB | 1.00 |
| | | | | | | | | | | |
| DeserializeRegular | 1000 | 2,206.702 μs | 3,277.4488 μs | 179.6480 μs | 1.00 | 0.00 | 70.3125 | 27.3438 | 586.17 KB | 1.00 |
| DeserializePropertyName | 1000 | 2,078.462 μs | 1,012.2441 μs | 55.4845 μs | 0.94 | 0.05 | 70.3125 | 27.3438 | 586.17 KB | 1.00 |
| DeserializeSourceGen | 1000 | 1,974.069 μs | 522.0887 μs | 28.6174 μs | 0.90 | 0.06 | 70.3125 | 27.3438 | 586.17 KB | 1.00 |
| | | | | | | | | | | |
| DeserializeRegular | 1000000 | 3,043,345.489 μs | 813,297.1786 μs | 44,579.5627 μs | 1.00 | 0.00 | 69000.0000 | 68000.0000 | 740994.09 KB | 1.00 |
| DeserializePropertyName | 1000000 | 3,053,464.635 μs | 634,603.0181 μs | 34,784.7328 μs | 1.00 | 0.01 | 69000.0000 | 68000.0000 | 741970.86 KB | 1.00 |
| DeserializeSourceGen | 1000000 | 3,036,496.137 μs | 1,641,846.7321 μs | 89,995.1596 μs | 1.00 | 0.03 | 69000.0000 | 68000.0000 | 740994.45 KB | 1.00 |
A little suprise here, JsonSourceGeneration
is a little bit slower than regular in most cases, but stability is better in some cases.
SourceGenerators
is faster than everything in serialization (up to 40% faster), but in deserialization it’s not so good in some cases.
So if you are facing some performance issues in serialization, you can use SourceGenerators
in serialization scenarios, and use regular deserialization.
Build Size
As We mentioned before, SourceGenerators
will size up the build package, let’s see how much it will size up.
I build a empty project with a single class defined, and then build the project with and without SourceGenerators
.
The result is:
- Without
SourceGenerators
: 5632 B - With
SourceGenerators
: 18432 B
We can see that SourceGenerators
will size up the build package by 13 KB. It’s not a big deal for most cases, and it’s a good trade-off for hugo performance improvement.