Using JSON for polymorphic types in C#

Luke Canvin

The scenario

Using Blazor, code written for the front-end has access to all the language features of C#. In particular, it may be useful to have a type hierarchy and make use of polymorphism. For example, in our design of the form delivery we based the form description around an abstract FormElement class from which were derived Field and FieldGroup.

Form classes

These same classes may be used on the server and on the client, however the data in their instances must still be transferred to the browser in a serialized form, with JSON being a popular choice.

This introduces a difficulty: a JSON object has no explicit type. Deserialization may know which base type to expect, but cannot instantiate the correct subtype without additional information.

A simple solution

In the Dev Camp project, we decided to make use of an inbuilt feature of Newtonsoft’s JSON library. There is a setting, TypeNameHandling, intended for this purpose.

new JsonSerializerSettings

The options are:

  • None – Do not include the .NET type name when serializing types.
  • Objects – Include the .NET type name when serializing into a JSON object structure.
  • Arrays – Include the .NET type name when serializing into a JSON array structure.
  • All – Always include the .NET type name when serializing.
  • Auto – Include the .NET type name when the type of the object being serialized is not the same as its declared type.

When using this setting, the serialized JSON includes additional fields that specify the actual type:

JSON with class type

A simple test to serialize and immediately deserialize this JSON shows that it maintains the correct subtype.

A fragile result

However, when connecting the Blazor client to the API we encountered a problem. Notice that the type field in the JSON above also includes the assembly name. Our DTO classes were in a Shared Project: the code gets compiled separately as part of each assembly that references it and that means the server calls the class “DTO.SelectField, Application.Api”, but the client would call it “DTO.SelectField, BlazorApp”. This mismatch causes deserialization to fail.

Our fix was to convert the shared code into its own class library. This means both the client and the server now understand the class as being “DTO.SelectField, SharedDtoLibrary”. The deserialization works and the classes pass smoothly from client to server with their actual types preserved.

Unfortunately, we ran into further problems because Blazor projects must target .NET Standard, whereas our API targets .NET Core. As a consequence, some core .NET types like lists were seen as originating from different assemblies and would not deserialize.

Overall, this approach is low effort but it is too fragile to use for an interface between separate systems: the two systems become strongly coupled and differences between them cannot be accommodated. So what are the alternatives?

NuGet provides

One reasonably convenient option is a NuGet package, JsonSubTypes. This provides implementations of JsonConverter that can be plugged into Newtonsoft’s serializer to give support for polymorphic types. There are several approaches available to make the converter work for a specific type hierarchy and an example might be:

Classes with subtype converter

Here the ­_type property is used to have each subclass output its class name as part of the JSON. The configuration in the JsonSubtypesConverterBuilder defines the base type and the name of the field that discriminates subtypes.

With this setup, the code

Serializing and deserializing the classes

will produce the output:

Cat sound, Dog sound, Cat sound
Cat sound, Dog sound, Cat sound

Build your own

Finally, for the most precise control, there is the option to build a custom converter. A class derived from JsonConverter must implement three abstract methods:

CanConvert(Type t) – a Boolean return value indicates whether the converter applies for a given source or target type. In the case above, we might return

Using IsSubclassOf

WriteJson , ReadJson – these methods perform the serialization and deserialization respectively. Both work with JSON token streams so they can be a little tricky to get to grips with but they allow exact control of the JSON representation.

Conclusion

Of these three methods, I would expect most cases to be suitable for the second approach using JsonSubtypes. To summarize the approaches:

TypeNameHandling

  • Inbuilt with Newtonsoft’s library
  • Single line of code to activate
  • Works well if the same piece of code is serializing and deserializing (e.g. to a data store)
  • Very fragile for interoperation of multiple systems.

JsonSubtypes

  • Easily downloaded as a NuGet package
  • Requires some modification or annotation of classes
  • Works between different systems that share a reference to the types being serialized.

Custom build

  • Newtonsoft supports a plugin approach
  • Precisely control the output
  • Only when easier approaches aren’t suitable