.NET Core JSON Serialization Changes – Newtonsoft vs System.Text.Json

I’ve been using Microsoft .NET for a long time. I started my programming journey learning C# on .NET 1.0 right after it’s initial release. In that time I have only experienced a breaking change twice. Once with WCF configuration in my app.config, and recently with the JSON serialization (or deserialization, as it were). For those upgrading a .NET Core 2.x application to .NET Core 3.x, you’ll want to be aware of some changes in the defaults. Today let’s talk about .NET Core and how it handles JSON serialization (and deserialization).

The crux of this issue is that prior to .NET Core 3.x, Newtonsoft is the default JSON engine. As of .NET Core 3.x, however, Microsoft switched to their implementation in System.Text.Json. If you are starting an application fresh with .NET Core 3.x you may not run into breaking changes. That said, you will still want to be aware of the limitations and changes in System.Text.Json.

Why change?

Microsoft has been packaging Newtonsoft Json.NET alongside .NET Core since it’s first release. While Newtonsoft Json.NET is great, Microsoft doesn’t control it. This means that it has it’s own developers, it’s own release cycle, and, more importantly, it’s own versioning scheme. As Microsoft is driving us towards .NET 5 and unification of the platform, they are standardizing on versioning. That, of course, isn’t the only reason but it’s still a pretty good one. It is easy to know which packages belong simply due to version parity with all the stuff 3.x+.

According to this blog post by Immo Landwerth, Microsoft’s primary reasons to build a new JSON library are:

  • To provide high-performance JSON APIs.
  • To remove the Json.NET dependency from ASP.NET Core.
  • To provide an ASP.NET Core integration package for Json.NET.

Rather than talk about their points–either in favor or opposition–I’m simply presenting them with the link and I’ll let you decide on your own how you feel about it. Grab some popcorn, read the article, and then read the comments.

You mentioned breaking changes?

As I alluded to in my introduction I’ve only personally ran into breaking changes on the .NET platform twice. That doesn’t mean there haven’t been more, just that I’ve only actually ran into them twice. The first time was something to do with WCF and configuration settings between the original .NET 3 and a later upgrade, possibly .NET 4. I genuinely don’t recall.

Of course the second time was what prompted this blog post in the first place. I’ll briefly paraphrase the scenario. We have an .NET Core API web application we originally started with .NET Core 2.2. We recently upgraded that API to .NET Core 3.1 along with some other very minor changes and released it to production. A week after it went to production we found out that a different API which called this one was having thousands of errors. Turns out they were validation “errors”, not actual errors, but I digress.

As I investigated the issue with a developer from the other team we both determined that our code for the API method in question had not changed since initial deployment. While talking I realized that what had changed, however, was the .NET Core version. I also knew that .NET Core 3 had changed the JSON serialization engine. Boom. That had to be it.

Digging deeper I learned that one of the differences between Newtonsoft Json.NET and System.Text.Json was that Newtonsoft could deserialize a long or int to a string property whereas System.Text.Json would skip the field. Similarly, if you pass a string that could otherwise be converted to an int or long, Newtonsoft would do so but System.Text.Json does not. This was our exact issue; API #2 was sending us a property as a long but our side had the field as a string (which is another story for another day).

Example

Here is just a small sample of code to highlight the problem:

// model in C#
public class Model 
{
    public string SomeProperty { get; set; }
}

// JSON message being posted to our API
{
    "someProperty": 12345
}

Migration Guide?

Running into that issue led me to investigate further. I found that MSDN actually has a table of differences in their migration guide. In my defense this migration guide appears with 01/10/2020 publication date and we had actually upgraded to .NET Core 3.0 prior to that date. Regardless, I now have a story on my Jira board to add some more integration tests to handle different JSON serialization formats for this particular API.

Case-Sensitivity

Christian Findlay, author of RestClient.Net, pointed out another breaking change I had missed. It turns out that System.Text.Json defaults to case sensitive deserialization. Newtonsoft is case-insensitive by default. I hadn’t run into this personally. I also never even noticed it buried in the documentation. Luckily you can easily change it to use case-insensitive matching via JsonSerializerOptions. Furthermore, you might find the benchmarks involved with case sensitivity interesting. Wade, from .NET Core Tutorials, posts What Those Benchmarks Of System.Text.Json Don’t Mention. Similarly, Christian alludes to speed during his owns tests while writing System.Text.Json Rest Client in RestClient.Net.

It is important to point out that the change to case-sensitivity is a performance boon. As Christian puts it, “The fact that JSON is treated as case insensitive has literally slowed the internet down.”

What can I do?

Luckily, and to their credit, Microsoft is looking at things in .NET Core in a whole new light compared to the olden days. What do I mean by this? Well, simply that the whole freaking framework is extensible. If you refer back to their migration guide, you’ll see that they give some advice for workarounds, etc.

Furthermore, you can always just outright skip System.Text.Json and go back to Newtonsoft JSON.Net really easily. That’s what I did. This Stackoverflow answer showed me how to do it. Basically, you just need to install the Microsoft.AspNetCore.Mvc.NewtonsoftJson package, then chain AddNewtonsoft() from AddControllers (AddControllersWithViews, AddMvcCore, or whatever other bootstrap variation you chose). Please note that if you already have AddJsonOptions on there you’ll want to remove it since this modifies System.Text.Json and can add it back to the loop.

Change the default .NET Core JSON Serialization back to Newtonsoft
Change the default .NET Core JSON Serialization back to Newtonsoft

Another option in this particular scenario that I ran into you can create a custom JsonConverter to handle the scenario. I’d be fine with that if I could be sure there aren’t other breaking changes. Simply put, I can’t spend the time right now to investigate any other potential side-effects.

Conclusion

While Microsoft has some good intentions with their new System.Text.Json namespace, if you were already using Newtonsoft JSON.NET you are likely to run into some unexpected changes. Many of them have decent workarounds, many don’t. In my experience I found it better and far easier to just switch back for now. It is important to recognize that the breaking changes are good overall. Encountering them in production, not so good.

Additional Reading

I searched the web and found I’m not alone. Here are a few resources you can use to make sure you cover all your bases before deciding what to do:

Credits

Photo by Damir Spanic on Unsplash