1

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);
    }
}
1
  • Your minimal reproducible example doesn't seem to address ReadJson() so I didn't discuss it in my answer. If you need help with that also, can you let us know where you are stuck (or ask a second question)? Commented Aug 19, 2024 at 19:02

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Perfect, thank you! Didn't know that property converters supersede converters applied to types.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.