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);
}
}
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:
By returning false
from CanRead
and CanWrite
, NoConverter
forces use of default serialization.
I used JsonConverter<T>
purely because it simplified the code a little.
Related questions:
Demo fiddle here.