Hello folks. In this article, I would like to illustrate why you no longer need to use the Newtonsoft.Json package to work with JSON.
Why don't I advise using Newtonsoft? The last release was on 8th March 2023. This project isn't developing and optimizing.
Now, there are three approaches to serializing and deserializing JSON.
I'll describe these approaches with code examples, make benchmarks, and show the performance difference. I'll also be using the latest version of .NET9
.
Preconditions
It would help to have a base knowledge of C# and .NET
. You should also install the BenchmarkDotNet
and Newtonsoft.Json
packages. And, of course, the .NET9
should also be installed.
Building base project
First, create a simple console application from the template and install the abovementioned packages.
Let's code. In a Program.cs
file, add this code:
internal static class Program
{
private static void Main()
{
BenchmarkRunner.Run<JsonPerformance>(new BenchmarkConfig());
}
}
This needed to run the benchmark. Further, let's add configuration for the benchmark.
public class BenchmarkConfig : ManualConfig
{
public BenchmarkConfig()
{
AddJob(Job.ShortRun
.WithRuntime(CoreRuntime.Core90)
.WithJit(Jit.Default)
.WithPlatform(Platform.X64)
);
AddLogger(ConsoleLogger.Default);
AddColumnProvider(BenchmarkDotNet.Columns.DefaultColumnProviders.Instance);
AddExporter(BenchmarkDotNet.Exporters.HtmlExporter.Default);
}
}
In this code, I have added the runtime, JIT, and platform. If, for some reason, you are unable to install .NET9
, you can use. .NET8
. Let's look at the methods below. AddLogger
is needed for the logging to the console. It is needed if you want to see the results of the performance tests in the console. AddColumnProvider
is needed to generate the results in the table. AddExporter
needs to export to the format you are interested in. I have chosen the HTML format. You can choose, for example, the Markdown format.
Next, let's create a simple model that we serialize to JSON.
public record Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public double YearsExperience { get; set; }
public List<string> Languages { get; set; }
public bool OpenToWork { get; set; }
}
Next step, we need to seed data.
public static class Generator
{
public static List<Person> GeneratePersons(int count)
{
var persons = new List<Person>();
for (int i = 0; i < count; i++)
{
persons.Add(GeneratePerson());
}
return persons;
}
private static Person GeneratePerson()
{
var random = new Random();
var randomNumber = random.Next(1, 100);
var person = new Person
{
FirstName = Guid.NewGuid().ToString(),
LastName = Guid.NewGuid().ToString(),
Age = randomNumber,
Languages = ["English", "French", "Spanish"],
YearsExperience = random.NextDouble(),
OpenToWork = randomNumber % 2 == 0
};
return person;
}
}
Building write and read JSON methods
Before that, let's create a new class, such as JsonPerformance
. It will contain our methods. Into this class, put these properties that will keep serialized and deserialized data:
private List<Person> Persons { get; } = Generator.GeneratePersons(100000);
private string? Json { get; set; }
Let's start with the realization of Newtonsoft's methods. It's typical operations with which you are likely familiar.
[Benchmark]
public void NewtonJsonWrite()
{
Json = JsonConvert.SerializeObject(Persons);
}
[Benchmark]
public void NewtonJsonRead()
{
if (Json == null) return;
var persons = JsonConvert.DeserializeObject<List<Person>>(Json);
}
In the next step, let's add methods using tools built into the framework. This realization is similar to Newtonsoft's.
[Benchmark]
public void FrameworkJsonWrite()
{
Json = JsonSerializer.Serialize(Persons);
}
[Benchmark]
public void FrameworkJsonRead()
{
if (Json == null) return;
var persons = JsonSerializer.Deserialize<List<Person>>(Json);
}
Finally, the last method is implemented with the manual approach, where you need to read and write data manually.
[Benchmark]
public void ManualJsonWrite()
{
var builder = new StringBuilder();
var writer = new StringWriter(builder);
using (var textWriter = new JsonTextWriter(writer))
{
textWriter.WriteStartArray();
foreach (var person in Persons)
{
textWriter.WriteStartObject();
textWriter.WritePropertyName("FirstName");
textWriter.WriteValue(person.FirstName);
textWriter.WritePropertyName("LastName");
textWriter.WriteValue(person.LastName);
textWriter.WritePropertyName("Age");
textWriter.WriteValue(person.Age);
textWriter.WritePropertyName("YearsExperience");
textWriter.WriteValue(person.YearsExperience);
textWriter.WritePropertyName("Languages");
textWriter.WriteStartArray();
foreach (var language in person.Languages)
{
textWriter.WriteValue(language);
}
textWriter.WriteEndArray();
textWriter.WritePropertyName("OpenToWork");
textWriter.WriteValue(person.OpenToWork);
textWriter.WriteEndObject();
}
textWriter.WriteEndArray();
}
Json = builder.ToString();
}
[Benchmark]
public void ManualJsonRead()
{
if (Json == null) return;
var persons = new List<Person>();
using var reader = new StringReader(Json);
using var jsonReader = new JsonTextReader(reader);
jsonReader.Read();
while (jsonReader.Read())
{
if (jsonReader.TokenType == JsonToken.StartObject)
{
var person = new Person();
while (jsonReader.Read())
{
if (jsonReader.TokenType == JsonToken.PropertyName)
{
var propertyName = jsonReader.Value?.ToString();
jsonReader.Read();
switch (propertyName)
{
case "FirstName":
person.FirstName = jsonReader.Value?.ToString() ?? string.Empty;
break;
case "LastName":
person.LastName = jsonReader.Value?.ToString() ?? string.Empty;
break;
case "Age":
person.Age = Convert.ToInt32(jsonReader.Value);
break;
case "YearsExperience":
person.YearsExperience = Convert.ToInt32(jsonReader.Value);
break;
case "Languages":
person.Languages = jsonReader.Value?.ToString()?.Split(',').ToList() ?? new List<string>();
break;
case "OpenToWork":
person.OpenToWork = Convert.ToBoolean(jsonReader.Value);
break;
}
}
else if (jsonReader.TokenType == JsonToken.EndObject)
{
break;
}
}
persons.Add(person);
}
else if (jsonReader.TokenType == JsonToken.EndArray)
{
break;
}
}
}
Performance
When we implement all methods, we can check performance. I'll take a series of benchmarks with large quantities of items, starting from 100000 items. Check this out. Before we start, you should switch your project to Release mode. Let's go.
As you can see, Newtonsoft performs lower than other methods. It is lowest in writing and faster in reading. The manual approach performs similarly to the system approach in writing but is lower in reading.
Conclusions
I recommend using a system tool to work with JSON. It is faster than other approaches and is a part of the framework that constantly develops and optimizes for the latest version of .NET
.
Thanks a lot for reading, and happy coding.
The source code is HERE.
Top comments (5)
"I recommend using a system tool" - hm? A system tool? You rather mean "a class from the system namespace"?
Sure you should use the System.Text.Json package these days, but there are some good reasons to still stick with Newtonsoft.Json. One of the reasons is that System still has no support for JSON Schema, which is super important for many scenarios.
You can use the "JsonSchema.Net" package that it uses "System.Text.Json." Anyway, it'll be faster than Newtonsoft.Json, which has not been supported for 1.5 years.
"Why don't I advise using Newtonsoft? The last release was on 8th March 2023. This project isn't developing and optimizing."
You have now created a little bug inside my brain... when is a software project done? You did a performance test where the differences are in nanoseconds, does a software project really need to continue optimizing until it's in another realm of performance? would that be enough to consider it done? 🤔
Regardless of that, I think this is valid consideration if you're creating a project where your potential clients have horrible internet but if you're aiming for normal internet users I'm not sure if it matters too much! I could be wrong though. Anyway, great post!! :3
Faster... but is it easier?
I think that's what NewtonSoft.Json is doing: instead of having to constantly write/rewrite plumbing to get your object de/serialized, it's giving you a method to convert it.
In other words, it's not entirely apples to apples, isn't it? What methods are there for doing a drop-in, generic object de/serialization?
Today I knew about JsonPerformance class, thankyou Sergii!