Recursive Polymorphic Deserialization with System.Text.Json

,

Goal: Deserialize a nested JSON structure, where the objects instantiated are not instances of the given abstract base type but rather are of the appropriate derived types.

In other words, if the specified destination type is AbstractBaseTpe, which should be returned is not an instance of that class but rather an instance of ChildTypeA or ChildTypeB or GrandchildType1, etc., as appropriate. This polymorphic deserialization behavior should hold true at the root level (that is, for the type directly returned by JsonSerializer.Deserialize<AbstractBaseType>(…)), as well as recursively for nested properties (that is, anytime the destination property is of type AbstractBaseType, the object instantiated for the property should be of the appropriate derived type, like ChildTypeA, etc.).

Hopefully, one day support for something along these lines will be built into System.Text.Json. However, for now, when we need it, we have craft our own solution (or borrow one from someone else).

In my case, the other day I needed a simple solution for polymorphic deserialization. Performance wasn’t critical. Just something simple that worked.

If it helps you, here’s one way to pull this off:

using System.Text.Json;
using System.Text.Json.Serialization;
 
public class TypeDiscriminatingConverter<T> : JsonConverter<T>
{
    public delegate Type TypeDiscriminatorConverter(ref Utf8JsonReader reader);
    private readonly TypeDiscriminatorConverter Converter;
 
    public TypeDiscriminatingConverter(TypeDiscriminatorConverter converter) =>
        (Converter) = (converter);
 
    public override bool CanConvert(Type typeToConvert) =>
        typeToConvert == typeof(T);
 
    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var typeCalculatorReader = reader;
        var actualType = Converter(ref typeCalculatorReader);
        
        return (T?)JsonSerializer.Deserialize(ref reader, actualType, options);
    }
 
    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
        throw new NotImplementedException();
}

How It Works

In a nutshell:

  1. You create an instance of the converter, providing a delegate that is given a JSON reader and which returns the actual type to deserialize into. The logic used here can be as simple or fancy as you’d like.
  2. System.Text.Json‘s deserialization functionality calls the converter when it’s asks to output an instance of type T, including when it needs to do so for a nested property inside of something it is already deserializing.
  3. In turn, the converter calls your delegate, then uses the type it replies with to tell System.Text.Json which type to actually deserialize into.

Simple Use Example

using System.Text.Json;
      
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(
  new TypeDiscriminatingConverter<AbstractBaseType>(
    (ref Utf8JsonReader reader) => {
      using var doc = JsonDocument.ParseValue(ref reader);
      var typeDiscriminator = doc.RootElement.GetProperty("kind").GetString();
 
      return typeDiscriminator switch
      {
        "A" => typeof(ChildTypeA),
        "ChildTypeB" => typeof(ChildTypeB),
        "A-Grandchild1" => typeof(GrandchildType1),
        _ => throw new JsonException()
      };
    }
  )
);
            
var output = JsonSerializer.Deserialize<AbstractBaseType>(json, serializeOptions);

Since performance wasn’t critical in my context, having the JSON parsed twice (once by JsonDocument.ParseValue in the discriminator-to-type delegate, then by JsonSerializer.Deserialize in the converter class itself), was an acceptable tradeoff to gain simplicity. For a performance-critical scenario, you may want to consider an alternative, such as having your delegate directly use Utf8JsonReader to read only as much as is absolutely necessary to compute the type to use.

The above is just a basic example of a discriminator-to-type delegate. In some cases, you could go much fancier, perhaps even reading the name of the class to use directly out of the JSON, then dynamically creating an instance of that class using its textual name. If you go this route, beware of the security implications. It would be very important to somehow limit what can be instantiated to only the expected set of classes to guard against an attacker using manipulated JSON to create instances of types you didn’t anticipate.

Leave a Reply

Your email address will not be published. Required fields are marked *