Search code examples
c#jsonjson.netpolymorphism

How to implement a polymorphic Newtonsoft.Json.JsonConverter?


Given the following polymorphic type hierarchy:

[JsonConverter(typeof(PolymorphicConverter<Base>))]
public record Base
{
    private Base() {}

    public record A(string Value) : Base;
    public record B(int Foobar) : Base;
    public record C(Base Recursive) : Base;
}

with the desired serialized json for e.g. A:

{
  "type": "A",
  "content": {
    "value": "jalla"
  }
}

Is it possible to create an implementation for PolymorphicConverter that's not tied to the type Base, is performant and is thread safe?

This is an implementation that works and is performant, but not thread safe:

using System;
using Newtonsoft.Json;

Base a = new Base.A("foobar");

var json = JsonConvert.SerializeObject(a);

Console.WriteLine(json);

[JsonConverter(typeof(PolymorphicConverter<Base>))]
public record Base
{
    private Base() {}

    public record A(string Value) : Base;
    public record B(int Foobar) : Base;
    public record C(Base Recursive) : Base;
}


public class PolymorphicConverter<TBase> : JsonConverter where TBase : class
{
    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        var type = value?.GetType() ?? throw new NotImplementedException();
        var converter = serializer.ContractResolver.ResolveContract(type).Converter;
        serializer.ContractResolver.ResolveContract(type).Converter = null;
        writer.WriteStartObject();
        writer.WritePropertyName("type");
        writer.WriteValue(type.Name);
        writer.WritePropertyName("content");
        serializer.Serialize(writer, value);
        serializer.ContractResolver.ResolveContract(type).Converter = converter;
        writer.WriteEndObject();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(TBase).IsAssignableFrom(objectType);
    }
}

Solution

  • You can take advantage of the feature that converters that are applied to a property supersede converters applied to types, or in settings, to serialize a DTO containing your TBase value without using its applied converter:

    public class PolymorphicConverter<TBase> : JsonConverter<TBase> where TBase : class
    {
        class DTO
        {
            [JsonProperty(Order = 1)]
            public string? type { get; set; }
            [JsonProperty(Order = 2, ReferenceLoopHandling = ReferenceLoopHandling.Serialize), 
             JsonConverter(typeof(NoConverter))]
            public TBase? content { get; set; }
        }
        
        public override void WriteJson(JsonWriter writer, TBase? value, JsonSerializer serializer)
        {
            var type = value?.GetType() ?? throw new NotImplementedException();
            serializer.Serialize(writer, new DTO { type = type.Name, content = value });
        }
    
        public override TBase? ReadJson(JsonReader reader, Type objectType, TBase? existingValue, bool hasExistingValue, JsonSerializer serializer) =>
            throw new NotImplementedException();
    }
    
    public class NoConverter : JsonConverter
    {
        // NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
        // By https://stackoverflow.com/users/3744182/dbc
        // To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
        public override bool CanConvert(Type objectType) => throw new NotImplementedException(); /* This converter should only be applied via attributes */
        public override bool CanRead => false;
        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => throw new NotImplementedException();
        public override bool CanWrite => false;
        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException();
    }
    

    This eliminates the thread-unsafe setting of serializer.ContractResolver.ResolveContract(type).Converter to null.

    Notes:

    Demo fiddle here.