Measuring the Performance of JSON Alternatives in .NET
Introduction
In this post I'll be talking about JSON serialisation performance using the base JSON classes included in .NET (System.Text.Json) as well as what used to be the de facto standard for serialisation, and was even included in the .NET templates, Newtonsoft.Json. I was curious to find out if there were any big differences in performance between the different APIs.
For starters, this is the type and instance that we'll be serialising in these examples:
public record Data(int Id, string Name, DateTime Timestamp);
var data = new Data(1, "Ricardo", DateTime.Now);
Lets start first with Newtonsoft.Json!
Newtonsoft.Json
Newtonsoft.Json has been around for a while, and it was once the default JSON serialiser for .NET, including .NET Framework and .NET Core. It has lots of features, and the new .NET API System.Text.Json hasn't (yet) come close to them. It essentially contains two different serialisers/deserialisers:
The way to use each is, for JsonConvert:
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data);
data = Newtonsoft.Json.JsonConvert.DeserializeObject<Data>(json);
And for JsonSerializer:
var serializer = new Newtonsoft.Json.JsonSerializer();
var sb = new StringBuilder();
serializer.Serialize(new StringWriter(sb), data);
data = serializer.Deserialize<Data>(new JsonTextReader(new StringReader(json)));
Both are available from the Newtonsoft.Json Nuget package and both allow passing additional options, for example, for indenting the output, controlling the casing of generated properties, dealing with missing/unexpected properties, serialising fields/properties, etc. I won't go into detail, but you can read more here.
System.Text.Json
.NET included its own JSON serialiser since .NET Core 3, System.Text.Json, which is now becoming the default serialiser for ASP.NET Core projects. It was built from scratch and has continued to evolve in later versions, with a big focus on performance. It offers essentially two serialisation/deserialisation classes:
Both can be used as this:
var json = JsonSerializer.Serialize(data);
data = JsonSerializer.Deserialize<Data>(json);
And a more simplified API, which allows in-place modification of properties:
var data = JsonNode.Parse(json);
var json = data.ToJsonString();
System.Text.Json APIs are available from the System.Text.Json Nuget package, which is typically already referenced, in the case of ASP.NET Core projects. ASP.NET Core however, being highly extensible, does allow the choice of the default serialiser/deserialiser.
There is a guide for those willing to migrate from Newtonsoft.Json to the new .NET API, which you can follow here, and the guide for using it is available here.
Source Generator
Source generators are a relatively new feature introduce in .NET 5. In a nutshell, it allows us to pre-generate code - partial classes or methods - based on some attributes we place in our code. The idea is to speed things up and provide all the complex information beforehand, which will be used at runtime.
.NET offers source generator support for JSON, which supposedly speeds up serialising and deserialising classes to and from JSON, because it pre-prepares a programmatic context with all the properties that will be serialised/deserialised.
In order to create a context, we create a class that inherits from JsonSerializerContext:
[JsonSerializable(typeof(Data), GenerationMode = JsonSourceGenerationMode.Metadata)]
internal partial class DataSourceGenerationContext : JsonSerializerContext { }
And use it like this:
var json = JsonSerializer.Serialize(data, DataSourceGenerationContext.Default.Data);
This way the Serialize method receives the context that describes the method to serialise. The DataSourceGenerationContext class is created for us, and you can actually see the generated code, if you navigate to it on Visual Studio.
Benchmarks
Now, let's go to the benchmark. We will be comparing the following methods:
Serialisation
So, these are the serialisation methods we will be using:
- JsonSerializer.Serialize (with and without the source generator context)
- JsonNode.ToJsonString
- JsonConvert.SerializeObject
- JsonSerializer.Serialize
Deserialisation
And the deserialisation methods:
- JsonSerializer.Deserialize (with and without the source generator context)
- JsonNode.Parse
- JsonConvert.DeserializeObject
- JsonSerializer.Deserialize
BenchmarkDotNet
We will be using, of course, BenchmarkDotNet, this is the default framework for performing these kinds of tests. Here is the code for the benchmarks:
[Config(typeof(AntiVirusFriendlyConfig))]
[Orderer(Order.SummaryOrderPolicy.FastestToSlowest)]
public class Benchmark
{
private static readonly Data data = new Data { Id = 1, Name = ".NET Serialization Benchmark", Timestamp = DateTime.Now };
private static readonly string json = "{\"Id\":1,\"Name\":\"Ricardo\",\"Timestamp\":\"2025-02-17T13:45:53.2140627+00:00\"}";
private static readonly Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
private static readonly System.Text.Json.Nodes.JsonNode? jn = System.Text.Json.Nodes.JsonNode.Parse(json);
private static StringBuilder sb = new StringBuilder();
private static StringWriter sw = new StringWriter(sb);
private static StringReader sr = new StringReader(json);
private static Newtonsoft.Json.JsonReader jr = new Newtonsoft.Json.JsonTextReader(sr);
[Benchmark]
public void NewtonsoftJsonSerializeObject()
{
Newtonsoft.Json.JsonConvert.SerializeObject(data);
}
[Benchmark]
public void NewtonsoftJsonDeserializeObject()
{
Newtonsoft.Json.JsonConvert.DeserializeObject(json);
}
[Benchmark]
public void NewtonsoftJsonSerialize()
{
serializer.Serialize(sw, data);
}
[Benchmark]
public void NewtonsoftJsonDeserialize()
{
serializer.Deserialize<Data>(jr);
}
[Benchmark]
public void SystemTextJsonSerialize()
{
System.Text.Json.JsonSerializer.Serialize(data);
}
[Benchmark]
public void SystemTextJsonDeserialize()
{
System.Text.Json.JsonSerializer.Deserialize<Data>(json);
}
[Benchmark]
public void SystemTextJsonSerializeSourceGenerator()
{
System.Text.Json.JsonSerializer.Serialize(data, DataSourceGenerationContext.Default.Data);
}
[Benchmark]
public void SystemTextJsonDeserializeSourceGenerator()
{
System.Text.Json.JsonSerializer.Deserialize<Data>(json, DataSourceGenerationContext.Default.Data);
}
[Benchmark]
public void SystemTextJsonNodeParse()
{
System.Text.Json.Nodes.JsonNode.Parse(json);
}
[Benchmark]
public void SystemTextJsonNodeToJsonString()
{
jn.ToJsonString();
}
[IterationSetup]
public void IterationSetup()
{
sr = new StringReader(json);
jr = new Newtonsoft.Json.JsonTextReader(sr);
}
}
public class AntiVirusFriendlyConfig : ManualConfig { public AntiVirusFriendlyConfig() { AddJob(Job.MediumRun.WithToolchain(InProcessNoEmitToolchain.Instance)); } }
I needed some configuration to get rid of some anti-virus annoyance (AntiVirusFriendlyConfig), and I also had to setup some iteration code that runs after each test (IterationSetup), to reset the string and text readers, because otherwise the memory consumption increases a lot. I am ordering the results by performance.
And to run the benchmarks we have:
public static void Main(string[] args) => BenchmarkRunner.Run<Benchmark>(args: args);
The results I got were:
| Method | Mean | Error | StdDev | Median | |----------------------------------------- |----------:|----------:|----------:|----------:| | SystemTextJsonNodeParse | 7.439 us | 0.3334 us | 0.4781 us | 7.275 us | | SystemTextJsonNodeToJsonString | 8.679 us | 1.0108 us | 1.4816 us | 7.900 us | | SystemTextJsonSerializeSourceGenerator | 11.311 us | 1.6442 us | 2.3049 us | 10.600 us | | SystemTextJsonDeserialize | 11.861 us | 1.0935 us | 1.5683 us | 11.200 us | | SystemTextJsonDeserializeSourceGenerator | 13.963 us | 0.4985 us | 0.6988 us | 13.700 us | | NewtonsoftJsonDeserialize | 14.233 us | 0.5714 us | 0.8010 us | 14.100 us | | SystemTextJsonSerialize | 14.269 us | 1.8099 us | 2.6529 us | 14.200 us | | NewtonsoftJsonSerialize | 15.807 us | 1.2978 us | 1.8613 us | 14.950 us | | NewtonsoftJsonSerializeObject | 16.514 us | 3.8517 us | 5.6458 us | 15.000 us | | NewtonsoftJsonDeserializeObject | 18.921 us | 1.7060 us | 2.4467 us | 17.950 us |
So, the results are:
Serialisation
- SystemTextJsonNodeToJsonString (JsonNode.ToJsonString)
- SystemTextJsonSerializeSourceGenerator (JsonSerializer.Serialize with source generator context)
- SystemTextJsonSerialize (JsonSerializer.Serialize)
- NewtonsoftJsonSerialize (JsonSerializer.Serialize)
- NewtonsoftJsonSerializeObject (JsonConvert.SerializeObject)
Deserialisation
- SystemTextJsonNodeParse (JsonNode.Parse)
- SystemTextJsonDeserialize (JsonSerializer.Deserialize)
- SystemTextJsonDeserializeSourceGenerator (JsonSerializer.Deserialize with source generator context)
- NewtonsoftJsonDeserialize (JsonSerializer.Deserialize)
- NewtonsoftJsonDeserializeObject (JsonConvert.DeserializeObject)
Conclusion
On your system these number can, of course, be different than mines. This came as a bit of a surprise to me, as I was thinking that the source generator version of System.Text.Json would be a lot faster, but I guess this is because the Serialization mode (fast path) is still not implemented. But I was expecting Newtonsoft.Json to be a bit slower, and it proved so. These results tend to change a bit, so the ordering may not be exactly the same, but the trend is consistent: JsonNode is the winner, followed by the source generator version of System.Text.Json. For what it's worth, you can use this benchmark, if performance is an issue, but this leaves out advanced serialisation features, in which Newtonsoft.Json is still the #1. I didn't got to test other options, such as searching for elements, or making modifications, perhaps on a future post.
Comments
Post a Comment