ASP.NET Core 3.0 – Custom JsonConverter For The New System.Text.Json

With the introduction of ASP.NET Core 3.0 the default JSON serializer has been changed from Newtonsoft.Json to System.Text.Json. For projects and libraries switching to the new JSON serializer this change means more performance and the opportunity to rewrite our JsonConverters.

In diesem Artikel:

pg
Pawel Gerr ist Architekt und Consultant bei Thinktecture. Er hat sich auf .NET Core Backends spezialisiert und kennt Entity Framework von vorne bis hinten.

Serialization of concrete classes

Let’s start with a simple one that can (de)serialize a concrete class Category. In our example we (de)serialize the property Name only.

				
					public class Category
{
   public string Name { get; }

   public Category(string name)
   {
      Name = name;
   }
}
				
			

To implement a custom JSON converter we have to derive from the generic class JsonConverter<T> and to implement 2 methods: Read and Write.

				
					public class CategoryJsonConverter : JsonConverter<Category>
{
   public override Category Read(ref Utf8JsonReader reader, 
                                 Type typeToConvert,
                                 JsonSerializerOptions options)
   {
      var name = reader.GetString();

      return new Category(name);
   }

   public override void Write(Utf8JsonWriter writer,
                              Category value,
                              JsonSerializerOptions options)
   {
      writer.WriteStringValue(value.Name);
   }
}

				
			

The method Read is using the Utf8JsonReader to fetch a string, i.e. the name, and the method Write is writing a string using an instance of Utf8JsonWriter.

In both cases (i.e. during serialization and deserialization) the converter is not being called if the value is null so I skipped the null checks. The .NET team doesn’t do null checks either, see JsonKeyValuePairConverter<TKey, TValue>.

Let’s test the new JSON converter. For that we create an instance of JsonSerializerOptions and add our CategoryJsonConverter to the Converters collection. Next, we use the static class JsonSerializer to serialize and to deserialize an instance of Category.

				
					Category category = new Category("my category");

var serializerOptions = new JsonSerializerOptions
{ 
    Converters = { new CategoryJsonConverter() }
};

// json = "my category"
var json = JsonSerializer.Serialize(category, serializerOptions); 

// deserializedCategory.Name = "my category"
var deserializedCategory = JsonSerializer.Deserialize<Category>(json, serializerOptions);
				
			

Serialization of generic classes

The next example is slightly more complex. The property we are serializing is a generic type argument, i.e. we can’t use methods like reader.GetString() or writer.WriteStringValue(name) because we don’t know the type at compile time.

In this example I’ve changed the class Category to a generic type and renamed the property Name to Key:

				
					public class Category<T>
{
   public T Key { get; }

   public Category(T key)
   {
      Key = key;
   }
}
				
			

For serialization of the generic property Key we need to fetch a JsonSerializer<T> using the instance of JsonSerializerOptions.

				
					public class CategoryJsonConverter<T> : JsonConverter<Category<T>>
{
   public override Category<T> Read(ref Utf8JsonReader reader,
                                    Type typeToConvert,
                                    JsonSerializerOptions options)
   {
      var converter = GetKeyConverter(options);
      var key = converter.Read(ref reader, typeToConvert, options);

      return new Category<T>(key);
   }

   public override void Write(Utf8JsonWriter writer,
                              Category<T> value,
                              JsonSerializerOptions options)
   {
      var converter = GetKeyConverter(options);
      converter.Write(writer, value.Key, options);
   }

   private static JsonConverter<T> GetKeyConverter(JsonSerializerOptions options)
   {
      var converter = options.GetConverter(typeof(T)) as JsonConverter<T>;

      if (converter is null)
         throw new JsonException("...");

      return converter;
   }
}
				
			

The behavior of the generic JSON converter is the same as before especially if the Key is of type string.

Deciding the concrete JSON converter at runtime

Having several categories with different key types, say, string and int, we need to register them all with the JsonSerializerOptions.

				
					var serializerOptions = new JsonSerializerOptions
                        {
                           Converters =
                           {
                              new CategoryJsonConverter<string>(),
                              new CategoryJsonConverter<int>()
                           }
                        };
				
			

If the number of required CategoryJsonConverters grows to big or the concrete types of the Key are not known at compile time then this approach is not an option. To make this decision at runtime we need to implement a JsonConverterFactory. The factory has 2 method: CanConvert(type) that returns true if the factory is responsible for the serialization of the provided type; and CreateConverter(type, options) that should return an instance of type JsonConverter.

				
					public class CategoryJsonConverterFactory : JsonConverterFactory
{
   public override bool CanConvert(Type typeToConvert)
   {
      if (!typeToConvert.IsGenericType)
         return false;

      var type = typeToConvert;

      if (!type.IsGenericTypeDefinition)
         type = type.GetGenericTypeDefinition();

      return type == typeof(Category<>);
   }

   public override JsonConverter CreateConverter(Type typeToConvert,
                                                 JsonSerializerOptions options)
   {
      var keyType = typeToConvert.GenericTypeArguments[0];
      var converterType = typeof(CategoryJsonConverter<>).MakeGenericType(keyType);

      return (JsonConverter)Activator.CreateInstance(converterType);
   }
}
				
			

Now, we can remove all registrations of the CategoryJsonConverter<T> from the options and add the newly implemented factory.

				
					Category<int> category = new Category<int>(42);

var serializerOptions = new JsonSerializerOptions
{
    Converters = { new CategoryJsonConverterFactory() }
};

// json = 42
var json = JsonSerializer.Serialize(category, serializerOptions);

// deserialized.Key = 42
var deserialized = JsonSerializer.Deserialize<Category<int>>(json, serializerOptions);
				
			

In the end the implementation of a custom converter for System.Text.Json is very similar to the one for Newtonsoft.Json. The biggest difference here is the non-existence of a non-generic JsonConverter but for that we’ve got the JsonConverterFactory.

Actually, there is a non-generic JsonConverter which is the base class of the JsonConverter<T> and the JsonConverterFactory but we cannot (and should not) use this class directly because its constructor is internal.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Database Access with Sessions
.NET
KP-round

Data Access in .NET Native AOT with Sessions

.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
Old computer with native code
.NET
KP-round

Native AOT with ASP.NET Core – Overview

Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
ASP.NET Core
favicon

Architektur-Modernisierung: Migration von WCF zu gRPC mit ASP.NET Core – ein pragmatischer Ansatz

Viele Projekte mit verteilten Anwendungen in der .NET-Welt basieren noch auf der Windows Communication Foundation (WCF). Doch wie kommt man weg von der "Altlast" und wie stellt man seinen Code auf sowohl moderne als auch zukunftssichere Beine? Eine mögliche Lösung ist gRPC.

13.04.2023
Blazor
favicon

gRPC Code-First mit ASP.NET Core 7 und Blazor WebAssembly

Wie in allen anderen browserbasierten Single-Page-Application (SPA) Frameworks, ist Blazor WebAssembly JSON-over-HTTP (über Web- oder REST-APIs) die bei weitem häufigste Methode, um Daten auszutauschen und serverseitige Vorgänge auszulösen. Der Client sendet eine HTTP-Anfrage mit JSON-Daten an eine URL, mitunter über unterschiedliche HTTP-Verben. Anschließend führt der Server eine Operation aus und antwortet mit einem HTTP-Statuscode und den resultierenden JSON-Daten. Warum sollte man das ändern? Nun, es gibt Gründe - vor allem wenn man in einem geschlossenen System ist und .NET sowohl im Frontend als auch im Backend einsetzt.
30.03.2023
Blazor
favicon

Blazor WebAssembly in .NET 7: UI-Performance-Optimierung auf Komponentenebene

Stockende UI, keine Reaktion nach dem Klick auf einen Button oder einer Eingabe in einem Feld - dies sind nur wenige Beispiele alltäglicher Probleme, die der Nutzung von Client-Anwendungen im Allgemeinen, und bei Webanwendungen im Speziellen, immer wieder auftreten können. In diesem Artikel schauen wir uns an, wie wir komponentenbasierte UIs in Blazor WebAssembly optimieren können, um dadurch eine für die Benutzer zufriedenstellende Geschwindigkeit und ein flüssiges UI zu bekommen.
29.03.2023
Blazor
sg

Understanding and Controlling the Blazor WebAssembly Startup Process

There are a lot of things going on in the background, when a Blazor WebAssembly application is being started. In some cases you might want to take a bit more control over that process. One example might be the wish to display a loading screen for applications that take some time for initial preparation, or when users are on a slow internet connection. However, in order to control something, we need to understand what is happening first. This article takes you down the rabbit hole of how a Blazor WASM application starts up.
07.03.2023