System.Text.Json, the new built-in JSON serializer in core 3.0

Pankaj Jha
Kongsberg Digital
6 min readJun 24, 2020

--

Whenever we speak of WebAPI, JSON comes as the default choice of message exchange format, and whenever we deal with JSON in .Net world, Newtonsoft package comes to help as default; but now with the release of .Net Core 3.0, Newtonsoft is no more the default.

The default serializer for JSON in .Net core 3.0 is System.Text.Json. Changing of the library is itself large enough for us to consider migrating our existing code, as we have been relying on NewtonSoft since long and the change will require us to revisit the effected code line by line.

Performance and high throughput is the key of any api or application to win the market. In order to have high performance application it becomes more important for us to use each component in the applications, which not only works but also works with best performance. Here System.Text.Json is best suited since it is highly tuned for performance gain. Let’s dive-in to see the steps of this migration and also look at some of the challenges while adopting this library.

Step 1: Changes in Startup config

To begin, we will modify the startup file in WebApi project where we configure middleware for all model’s serializer and deserializer.
Replace the default formatter “AddJsonFormatters()” with following code block:

.AddJsonOptions(options => {options.JsonSerializerOptions.WriteIndented = true;})

In case, we have SignalR in our application; then we will have to add JsonProtocol as shown in the below code block:

services.AddSignalR(options => {}).AddJsonProtocol(options => {options.PayloadSerializerOptions.WriteIndented = false;});

By doing the above we are asking the .Net to consider “System.JSON” as the default serializer for JSON which will have huge performance gain in serialization and deserialization of messages.

Step 2: Modification in the models

The below code block shows that JsonProperty attribute is replaced by JsonPropertyName

// As in Newtonsoftpublic class Model {[JsonProperty(“count”)]public ulong Count { get; set; }[JsonConverter(typeof(JsonDateTimeTickConverter))][JsonProperty(“timeLast”)]public DateTime TimeLast { get; set; }}//Alternative in System.Text.Json would bepublic class Model {[JsonPropertyName(“count”)]public ulong Count { get; set; }[JsonConverter(typeof(JsonDateTimeTickConverter))][JsonPropertyName(“timeLast”)]public DateTime TimeLast { get; set; }}

Step 3: Modification in the Custom JsonConverter

We need to implement typed System.Text.Json.Serialization .JsonConverter<T> for custom converters.

Step 4: Additional Custom JsonConverter

We would require some additional converters for some of the models which was being handled by default using Newtonsoft.

One of the examples where we would require a custom converter is given below, which has an abstract base class used for creating two types: named MyDerive and MyDerive1.
There is a model which has base class as its property, the model could be initialized with one of the derived property.

public abstract class MyBase {public int B1 { get; set; }public string B2 { get; set; }}public class MyDerive : MyBase {public int D1 { get; set; }public string D2 { get; set; }}public class MyDerive2 : MyBase {public int E1 { get; set; }public string E2 { get; set; }}public class Model {public MyBase prop { get; set; }}//initialize modelvar model = new Model() {prop = new MyDerive() {D1 = 1,D2 = “2”}//Serializevar json = JsonSerializer.Serialize(model);

To our surprise, the resultant json would not contain values from derive class, instead have base properties. To get this model serialized properly, we have to write a custom converter and then use the serializer as shown in the below code block.

class MyDeriveWriteOnlyJsonConverter : JsonConverter<MyBase> {public override void Write(Utf8JsonWriter writer,    JsonInCore3Tests.MyBase value, JsonSerializerOptions options) {JsonSerializer.Serialize(writer, value, value.GetType(), options);}
}

and then use the above serializer to serialize the mode as shown below:

var serializer = new JsonSerializerOptions(){ IgnoreNullValues = true};serializer.Converters.Add(new MyDeriveWriteOnlyJsonConverter());var json = JsonSerializer.Serialize(model, serializer);

Second example is of a model which has properties defined using interface. Here we have a type which implements an interface. The model uses interface as its property and model is initialized with concrete class as shown in the below code block

interface MyBaseInterface {}class MyBaseInterface1: MyBaseInterface {public int D1 { get; set; }public string D2 { get; set; }}class Model {public MyBaseInterface prop { get; set; }}var model = new Model(){prop = new MyBaseInterface1 {D1 = 1,D2 = “2”}};//Serializevar json = JsonSerializer.Serialize(model);

Similar to the first case we would be surprised to find that model serialized would not get values from MyBaseInterface1.

In order to get proper values in serialized string we again have to write a custom converter as the code block shown below:

class JsonInterfaceJsonConverter : JsonConverter<MyBaseInterface> {public override void Write(Utf8JsonWriter writer, JsonInCore3Tests.MyBaseInterface value, JsonSerializerOptions options) {JsonSerializer.Serialize(writer, value, value.GetType(), options);}}

Now if we serialize the model using this custom converter, we would get correct serialized string

var serializer = new JsonSerializerOptions(){ IgnoreNullValues = true};serializer.Converters.Add(new JsonInterfaceJsonConverter());var json = JsonSerializer.Serialize(model, serializer);

Let’s consider a third case where our api accepts a json which has a type property as shown below.

var jsonType1 = {“type”: “1”,”context”: “#2”,”name”: “Node1”}var jsonType2 = {“type”: “2”,”context”: “#2”,”name”: “Node1”,”description”: “Description1”}

Based on above json, either class Type1 or class Type2 will be initialized,
how do we do this?

class BaseType{}class Type1 : BaseType {string Context { get; set; }string Name { get; set; }}class Type2 : BaseType {string Context { get; set; }string Name { get; set; }string Description { get; set; }}

Again, custom converter would help us. We would create a converter of BaseType which would first read type parameter and then initialize concrete class as shown in the below code block:

public override BaseType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options){while (reader.Read()){switch (reader.TokenType){case JsonTokenType.PropertyName: {var text = reader.GetString();if (text == “type”){reader.Read();var type=reader.GetString()actualType = GetActualType(type, tmpReader, options);}break;}}}return actualType;}

Step 5: Parameters corresponding to JArray, JObject, JToken of Newtonsoft

JArray, JObject, JToken of Newtonsoft can be replaced with JsonElement of System.Text.Json in the webapi. Similarly JArray of Newtonsoft can be replaced by array of JsonElement(JsonElement[]).

There are some changes in the way we retrieve values from JsonElement which are shown in code snippet below.

//Newtonsoft versionJObject jsonModel[“selector”].Value<string>();//System.Text.Json versionJsonElement jsonModel.GetProperty(“selector”).GetString();//Newtonsoft version(JArray)jsonModel[“events”]).Select(em => em.ToIEvent())//System.Text.Json versionjsonModel.GetProperty(“events”)).Select(em => em.ToIEvent())

Drawbacks

One of the biggest drawback with System.Text.Json in core 3.0 is, it does not support nonstring key in dictionary which can be tested in a unit test which is shown in the below code block:

Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<Dictionary<int, int>>(@”{1:1}”));

To have workaround for this we would require to write Custom Converter which would converts nonstring key of dictionary into string which is being discussed in the link ( https://github.com/dotnet/corefx/issues/40120 ) but it does not consider all scenarios

The second drawback with System.Text.Json in core 3.0 is, it is stricter in deserializing json to object which means the existing code would break if json text will have mismatch in datatype which is shown in below code block:

public class SimpleModel{public int Id { get; set; }public string Name { get; set; }}Assert.DoesNotThrow(() => JsonSerializer.Deserialize<SimpleModel>(“{\”Id\”:1,\”Name\”:\”name\”}”));Assert.Throws<JsonException>(()=> JsonSerializer.Deserialize<SimpleModel>(“{\”Id\”:\”1\”,\”Name\”:\”name\”}”));

The third drawback with new library is with respect to single quote which worked fine with Newtonsoft but breaks with System.Text.Json which is shown in below code block:

Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<SimpleModel>(“{\”Id\”:1, \”Name\”: ’name’ }”));

As quoted in Microsoft documentation, “The library design emphasizes high performance and low memory allocation over an extensive feature set. Built-in UTF-8 support optimizes the process of reading and writing JSON text encoded as UTF-8, which is the most prevalent encoding for data on the web and files on disk.

The library also provides classes for working with an in-memory document object model (DOM). This feature enables random read-only access of the elements in a JSON file or string.”

Since this library is new and major focus of this library is performance and to become the one and only choice of every developer, when it comes to handling JSON in .Net world. There are some challenges in adopting this library but definitely worth incorporating it in all the projects.

--

--