JSON Serialization in .NET Framework 4.8 vs .NET 8

Trevor McCubbin
8 min readFeb 10, 2024

--

Generated with AI ∙ February 10, 2024

A few weeks ago, I wrote a Medium article exploring the performance differences between Newtonsoft.Json and System.Text.Json in .NET 8. The goal of the article was to see if there were any performance differences from switching from Newtonsoft.Json (Json.Net) to Microsoft’s built-in solution, System.Text.Json.

In this article, I concluded that System.Text.Json is significantly faster than Json.Net in all tested Serialization and Deserialization test cases, and it also consumed significantly less memory while doing it. All tests were conducted on .NET 8.0.0 and the latest version of the relevant dependencies.

But that led me to wonder: Do these performance increases also apply in .NET Framework 4.8, or are they specific to the .NET 8 runtime? Also, how far has performance come from using the Windows-locked .NET Framework to the cross-platform .NET 8?

I was curious to learn and thought I would share my findings for others who may be facing the decision of deciding if upgrading frameworks is worth the effort.

Performance Tests

Before we dive head-first into the comparison between JSON serialization libraries in .NET Framework 4.8 and .NET 8.0, let’s set the stage with an overview of our benchmarking approach. In this section, we’ll outline the structure of our tests and the metrics we’ll be examining.

Serialization

The serialization tests comprise two parts: the first focuses on serializing a large dataset all at once, while the other deals with serializing individual data, such as a single User model at a time.

[SimpleJob(
RuntimeMoniker.Net48,
launchCount: 1,
warmupCount: 3,
iterationCount: 5,
invocationCount: -1,
id: "NET Framework 4.8",
baseline: true
)]
[SimpleJob(
RuntimeMoniker.Net80,
launchCount: 1,
warmupCount: 3,
iterationCount: 5,
invocationCount: -1,
id: "NET 8.0"
)]
[MemoryDiagnoser(displayGenColumns: false)]
[HideColumns(Column.Job, Column.StdDev, Column.Error, Column.RatioSD)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[Orderer(SummaryOrderPolicy.SlowestToFastest, MethodOrderPolicy.Declared)]
public class JsonSerialization
{
[Params(10000)]
public int Count { get; set; }

private List<User> _testUsers = new List<User>();

[GlobalSetup]
public void GlobalSetup()
{
var faker = new Faker<User>().CustomInstantiator(
f =>
new User
{
UserId = Guid.NewGuid(),
FirstName = f.Name.FirstName(),
LastName = f.Name.LastName(),
FullName = f.Name.FullName(),
Username = f.Internet.UserName(f.Name.FirstName(), f.Name.LastName()),
Email = f.Internet.Email(f.Name.FirstName(), f.Name.LastName())
}
);

_testUsers = faker.Generate(Count);
}

[
BenchmarkCategory("Serialize Big Data"),
Benchmark(Description = "Newtonsoft", Baseline = true)
]
public void NewtonsoftSerializeBigData() =>
_ = Newtonsoft.Json.JsonConvert.SerializeObject(_testUsers);

[BenchmarkCategory("Serialize Big Data"), Benchmark(Description = "Microsoft")]
public void MicrosoftSerializeBigData() => _ = JsonSerializer.Serialize(_testUsers);

[
BenchmarkCategory("Serialize Individual Data"),
Benchmark(Description = "Newtonsoft", Baseline = true)
]
public void NewtonsoftSerializeIndividualData()
{
foreach (var user in _testUsers)
{
_ = Newtonsoft.Json.JsonConvert.SerializeObject(user);
}
}

[BenchmarkCategory("Serialize Individual Data"), Benchmark(Description = "Microsoft")]
public void MicrosoftSerializeIndividualData()
{
foreach (var user in _testUsers)
{
_ = JsonSerializer.Serialize(user);
}
}
}

Deserialization

The deserialization tests follow a similar structure to the serialization tests. One test handles deserialization of a large dataset, while the other focuses on deserializing individual data.

[SimpleJob(
RuntimeMoniker.Net48,
launchCount: 1,
warmupCount: 3,
iterationCount: 5,
invocationCount: -1,
id: "NET Framework 4.8",
baseline: true
)]
[SimpleJob(
RuntimeMoniker.Net80,
launchCount: 1,
warmupCount: 3,
iterationCount: 5,
invocationCount: -1,
id: "NET 8.0"
)]
[MemoryDiagnoser(displayGenColumns: false)]
[HideColumns(Column.Job, Column.StdDev, Column.Error, Column.RatioSD)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
[Orderer(SummaryOrderPolicy.SlowestToFastest, MethodOrderPolicy.Declared)]
public class JsonDeserialization
{
[Params(10000)]
public int Count { get; set; }

private string _serializedTestUsers = string.Empty;

private readonly List<string> _serializedTestUsersList = new List<string>();

[GlobalSetup]
public void GlobalSetup()
{
var faker = new Faker<User>().CustomInstantiator(
f =>
new User
{
UserId = Guid.NewGuid(),
FirstName = f.Name.FirstName(),
LastName = f.Name.LastName(),
FullName = f.Name.FullName(),
Username = f.Internet.UserName(f.Name.FirstName(), f.Name.LastName()),
Email = f.Internet.Email(f.Name.FirstName(), f.Name.LastName())
}
);

var testUsers = faker.Generate(Count);

_serializedTestUsers = JsonSerializer.Serialize(testUsers);

foreach (var user in testUsers.Select(u => JsonSerializer.Serialize(u)))
{
_serializedTestUsersList.Add(user);
}
}

[
BenchmarkCategory("Deserialize Big Data"),
Benchmark(Description = "Newtonsoft", Baseline = true)
]
public void NewtonsoftDeserializeBigData() =>
_ = JsonConvert.DeserializeObject<List<User>>(_serializedTestUsers);

[BenchmarkCategory("Deserialize Big Data"), Benchmark(Description = "Microsoft")]
public void MicrosoftDeserializeBigData() =>
_ = JsonSerializer.Deserialize<List<User>>(_serializedTestUsers);

[
BenchmarkCategory("Deserialize Individual Data"),
Benchmark(Description = "Newtonsoft", Baseline = true)
]
public void NewtonsoftDeserializeIndividualData()
{
foreach (var user in _serializedTestUsersList)
{
_ = JsonConvert.DeserializeObject<User>(user);
}
}

[BenchmarkCategory("Deserialize Individual Data"), Benchmark(Description = "Microsoft")]
public void MicrosoftDeserializeIndividualData()
{
foreach (var user in _serializedTestUsersList)
{
_ = JsonSerializer.Deserialize<User>(user);
}
}
}

Performance Results

Serialization

| Method     | Runtime            | Categories                | Count |      Mean | Ratio | Allocated | Alloc Ratio |
| ---------- | ------------------ | ------------------------- | ----- | --------: | ----: | --------: | ----------: |
| Newtonsoft | .NET Framework 4.8 | Serialize Big Data | 10000 | 17.736 ms | 1.00 | 8.16 MB | 1.00 |
| Microsoft | .NET Framework 4.8 | Serialize Big Data | 10000 | 17.481 ms | 0.99 | 5.42 MB | 0.66 |
| Newtonsoft | .NET 8.0 | Serialize Big Data | 10000 | 9.821 ms | 0.55 | 8.07 MB | 0.99 |
| Microsoft | .NET 8.0 | Serialize Big Data | 10000 | 5.909 ms | 0.33 | 3.42 MB | 0.42 |
| | | | | | | | |
| Microsoft | .NET Framework 4.8 | Serialize Individual Data | 10000 | 17.734 ms | 1.02 | 3.69 MB | 0.21 |
| Newtonsoft | .NET Framework 4.8 | Serialize Individual Data | 10000 | 17.293 ms | 1.00 | 17.69 MB | 1.00 |
| Newtonsoft | .NET 8.0 | Serialize Individual Data | 10000 | 7.670 ms | 0.44 | 17.14 MB | 0.97 |
| Microsoft | .NET 8.0 | Serialize Individual Data | 10000 | 5.636 ms | 0.33 | 3.64 MB | 0.21 |

The tests above are ordered from slowest to fastest execution speed and grouped by their individual capacity. The benchmark baseline was established using Newtonsoft.Json in .NET Framework 4.8 due to Newtonsoft’s overwhelming popularity in .NET Framework projects.

It appears from these results that the JSON package you choose to use in .NET Framework 4.8 doesn’t have much impact on the overall mean execution time but does come with noticeably lower memory allocation. However, in .NET 8, there is a clear speed and memory advantage when using System.Text.Json over Json.Net.

Comparing the slowest versus fastest execution times, if you decide to upgrade from .NET Framework 4.8 using Newtonsoft to .NET 8 using System.Text.Json, you could expect approximately a 65% reduction in mean execution time and a 60% reduction in memory usage for large datasets. The results are even more drastic for large collections of small data, where you could see around a 65% reduction in mean execution time and an 80% reduction in memory allocation.

Deserialization

| Method     | Runtime            | Categories                  | Count |      Mean | Ratio | Allocated | Alloc Ratio |
| ---------- | ------------------ | --------------------------- | ----- | --------: | ----: | --------: | ----------: |
| Newtonsoft | .NET Framework 4.8 | Deserialize Big Data | 10000 | 33.928 ms | 1.00 | 4.8 MB | 1.00 |
| Microsoft | .NET Framework 4.8 | Deserialize Big Data | 10000 | 28.431 ms | 0.84 | 5.21 MB | 1.08 |
| Newtonsoft | .NET 8.0 | Deserialize Big Data | 10000 | 21.099 ms | 0.63 | 4.52 MB | 0.94 |
| Microsoft | .NET 8.0 | Deserialize Big Data | 10000 | 12.996 ms | 0.39 | 5.01 MB | 1.04 |
| | | | | | | | |
| Newtonsoft | .NET Framework 4.8 | Deserialize Individual Data | 10000 | 29.528 ms | 1.00 | 29.96 MB | 1.00 |
| Microsoft | .NET Framework 4.8 | Deserialize Individual Data | 10000 | 23.407 ms | 0.79 | 3.25 MB | 0.11 |
| Newtonsoft | .NET 8.0 | Deserialize Individual Data | 10000 | 15.203 ms | 0.51 | 29.55 MB | 0.99 |
| Microsoft | .NET 8.0 | Deserialize Individual Data | 10000 | 8.211 ms | 0.28 | 3.05 MB | 0.10 |

These tests follow the same pattern as the serialization tests, where they are ordered in descending order by mean execution time and grouped by their appropriate category. The benchmark baseline remains identical to the serialization tests, where .NET Framework 4.8 with Newtonsoft serves as our reference point.

In these results, we observe that System.Text.Json is faster in both .NET Framework 4.8 and .NET 8. However, it appears that something unusual is happening with memory allocation for deserializing a large dataset, as the allocated memory is similar for all tests, contrary to what was observed in my previous article.

Upgrading from .NET Framework 4.8 with Newtonsoft to .NET 8 with a large dataset would decrease mean execution time by around 60%, but would not significantly affect memory allocation. However, when deserializing individual data, we see a significant decrease in execution time by around 70% and a reduction in memory allocation by around 90%.

Analyzing the Results

After running my.NET JSON serialization and deserialization performance tests, it’s time to dive into the results and understand what they mean.

Performance Metrics

First, let’s recap the key performance metrics we’ve gathered:

  • Serialization Time: This is the time it takes to convert .NET objects into JSON format.
  • Deserialization Time: This is the time it takes to convert JSON back into .NET objects.
  • Memory Usage: This is the amount of memory consumed during the serialization and deserialization processes.

Observations

Observations from the tests include:

  • Serialization and Deserialization Speed: System.Text.Json in .NET 8 demonstrates significantly higher speed than Newtonsoft.Json across all tested scenarios. This speed difference also holds true in .NET Framework 4.8, indicating that the performance enhancements are not exclusive to the .NET 8 runtime.
  • Memory Consumption: An unusual pattern was observed with memory allocation for deserializing a large dataset. The allocated memory is similar for all tests, contrary to what was observed in the previous article. However, when deserializing individual data, we see a significant reduction in memory allocation by around 90%.
  • Performance Improvement from .NET Framework 4.8 to .NET 8.0: The tests show a noticeable performance improvement when moving from the Windows-locked .NET Framework 4.8 to the cross-platform .NET 8.0. This suggests that upgrading frameworks could be worth the effort, especially for applications that heavily rely on JSON serialization and deserialization.

Conclusion

The results of my performance tests suggest that System.Text.Json is a more efficient library for JSON serialization and deserialization compared to Newtonsoft.Json, regardless of whether you’re using .NET Framework 4.8 or .NET 8.0. Therefore, if you’re considering whether to switch libraries or upgrade your framework, these performance improvements could be a compelling reason to do so.

However, it’s important to note that System.Text.Json does not have a drop-in replacement for all Newtonsoft.Json features. While Microsoft has made significant strides in improving invalid JSON deserialization in System.Text.Json, it still does not have full feature parity with Newtonsoft.Json. This means that while System.Text.Json may offer performance benefits, it may not be suitable for all use cases, particularly those that rely on specific features unique to Newtonsoft.Json.

Therefore, when deciding whether to switch from Newtonsoft.Json to System.Text.Json, it’s crucial to consider not only the performance implications but also the specific needs and requirements of your project. If your project heavily relies on features that are not yet supported by System.Text.Json, sticking with Newtonsoft.Json might be the better choice despite the potential performance gains.

If you found this article useful, feel free to read my other articles or give me a follow. All feedback and support is appreciated. You can view all benchmark code on my personal GitHub:

Connect with me on LinkedIn for more updates and discussions:

https://www.linkedin.com/in/trevor-mccubbin-6b1281202/

--

--

No responses yet