Skip to main content

Migrating An API from Newtonsoft.Json to System.Text.Json

In some recent work with Umbraco I’ve been looking to migrate an API from using the Newtonsoft.Json serialization library to the newer, Microsoft one, System.Text.Json.

The reason for doing this was to align the API we created for Umbraco Forms with the content delivery API available in Umbraco 12. We’re keen to ensure these APIs across Umbraco products are similar both in terms of behaviour but also behind the scenes when it comes to the libraries we are using.

As such I had a few updates to make – mostly straightforward, and some that needed a bit more research and development.

Deserialization via a Model Binder

When a form is submitted, a model binder is referenced in the controller’s action method to deserialize the incoming request to a strongly typed model:

Within the model binder we had the following code to deserialize the request into the appropriate type:

To convert this it was just necessary to change JsonConvert.DeserializeObject to JsonSerializer.Deserialize, and replace the Newtonsoft.Json using statement with System.Text.Json.

Defining Serialization Behaviour

In order to manage serialization settings such as null handling and property ordering, we make use of an attribute that decorates the API controllers at the class level.

The attribute applies settings for serializing the result objects and collections when a 200 “OK” response is returned:

  • Any properties that have null values are omitted from the response.
  • Dictionary keys, as well as all other properties, are camel-cased.
  • Enums are returned as strings.
  • The properties are returned in alphabetical order.

The Newstonsoft implementation requires a custom DefaultContractResolver, and looks like this:

To do the same thing with System.Text.Json was similar when it came to defining camel casing and null handling, but required a new feature available only in .NET 7 to be able to replicate the property ordering. Hat-tip to dbc on Stack Overflow for this answer.

Creating a JsonConverter

Another fiddly migration was in implementing a JsonConverter. These are classes that a responsible for converting an object to and from JSON. For primitive types and collections, you don’t need to do anything special as these conversions are automatic. But you may have a use case where you need to do something more than the default behaviour of the library provides.

The example we had was for handling a form submission where each value could either be a string, or a collection of strings. A “name” field for example would accept a single string input such as “Fred”, and a “favourite colours” multiple choice field could receive a collection, such as “Red”, “Green”.

We defined the Values property as a collection so needed the converter to populate it either with the collection posted (for multiple choice fields) or as a collection with a single element (for single value input fields).

Both libraries require decorating the property with an attribute indicating the converter to use. The only difference was the namespace needed for the using statement (Newtonsoft.Json or System.Text.Json.Serialization).

For the NewtonSoft implementation, it was necessary to inherit from the base JsonConverter class, and override the ReadJson method. Within that, we can examine the incoming raw JSON, determine if we have an array or a single value, and act according to set the values on the property.

The full class looked like this:

For System.Text.Json, there is a similar base class, but this time we have to provide type parameters. We get access to the raw JSON but this time as a stream that we examine and act on. By checking the token type at expected points, we can check if we have a single string value or the start of an array, and again process as appropriate.

Since it's release in .NET Core 3.1, migrating older projects to the newer System.Text.Json serialization library may have been, and may continue to be, common task for developers. It's fine to stick to the tried and tested, but to benefit from the improved performance, security, and standards compliance with the new library, it'll be necessary to move forward and use it. This article showed a few serialization features we were using and how we migrated to the newer library. There's likely many more uses that people will run into too... for which information is available at the Microsoft docs.