I can't figure out why the following code is throwing a StackOverflowException
during Write
. I want to be able to use a type discriminator to help me serialize / deserialize objects while not losing concrete implementation details. How can I fix this issue?
using System.Text.Json;
using System.Text.Json.Serialization;
public abstract class ClassC
{
public string PropertyC { get; set; }
public string TypeDiscriminator => this.GetType().FullName;
}
public class ClassB : ClassC
{
public string PropertyB { get; set; }
}
public class ClassA : ClassB
{
public string PropertyA { get; set; }
}
public class PolymorphicJsonConverter<TBase> : JsonConverter<TBase> where TBase : ClassC
{
public override bool CanConvert(Type typeToConvert) => typeof(TBase).IsAssignableFrom(typeToConvert);
public override TBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (var jsonDoc = JsonDocument.ParseValue(ref reader))
{
var root = jsonDoc.RootElement;
var typeDiscriminator = root.GetProperty("TypeDiscriminator").GetString();
var type = Type.GetType(typeDiscriminator);
if (type == null)
{
throw new JsonException($"Type {typeDiscriminator} could not be found.");
}
return (TBase)JsonSerializer.Deserialize(root.GetRawText(), type, options);
}
}
public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
public class Program
{
public static void Main()
{
var options = new JsonSerializerOptions
{
Converters = { new PolymorphicJsonConverter<ClassC>() },
WriteIndented = true
};
ClassA classA = new ClassA { PropertyA = "ValueA", PropertyB = "ValueB", PropertyC = "ValueC" };
string json = JsonSerializer.Serialize<ClassC>(classA, options);
ClassC deserialized = JsonSerializer.Deserialize<ClassC>(json, options);
Console.WriteLine(json);
Console.WriteLine(deserialized is ClassA); // Should output 'True'
}
}
Your problem is that your converter is calling itself recursively here:
return (TBase)JsonSerializer.Deserialize(root.GetRawText(), type, options);
The recursion occurs because your CanConvert(Type typeToConvert)
method returns true
for the concrete derived types of ClassC
even though no conversion is required for them.
Since your base type is abstract, your converter can be modified as follows, adding a check inside CanConvert
that the incoming type is abstract:
public class ClassCJsonConverter : JsonConverter<ClassC>
{
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsAssignableFrom(typeof(ClassC)) && typeToConvert.IsAbstract;
public override ClassC? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (var jsonDoc = JsonDocument.ParseValue(ref reader))
{
var root = jsonDoc.RootElement;
var typeDiscriminator = root.GetProperty("TypeDiscriminator").GetString();
var type = typeDiscriminator == null ? null : Type.GetType(typeDiscriminator);
if (type == null || CanConvert(type)) // Here we guarantee there is no infinite recursion.
throw new JsonException($"Type {typeDiscriminator} could not be found or is invalid.");
return (ClassC?)root.Deserialize(type, options);
}
}
public override void Write(Utf8JsonWriter writer, ClassC value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
The assumption made by this version is that any type assignable to ClassC
is either abstract (and so requires type resolution) or is concrete (and does not require type resolution). If you had non-abstract base types then this strategy would not work, and you would need to adopt a more complex strategy of disabling the converter during the recursive call such as the one shown in How to use default serialization in a custom System.Text.Json JsonConverter?.
Demo fiddle #1 here.
As an alternative, as of .NET 7, support for polymorphic type discriminators is built into System.Text.Json and no converter is required. See this answer to Is polymorphic deserialization possible in System.Text.Json?. Using this approach your types can be round-tripped with the following attributes applied:
[JsonDerivedType(typeof(ClassB), nameof(ClassB))]
[JsonDerivedType(typeof(ClassA), nameof(ClassA))]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "TypeDiscriminator")]
public abstract class ClassC
{
public string PropertyC { get; set; }
}
[JsonDerivedType(typeof(ClassB), nameof(ClassB))]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "TypeDiscriminator")]
public class ClassB : ClassC
{
public string PropertyB { get; set; }
}
[JsonDerivedType(typeof(ClassA), nameof(ClassA))]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "TypeDiscriminator")]
public class ClassA : ClassB
{
public string PropertyA { get; set; }
}
You may need to modify the type discriminator values passed to the [JsonDerivedType(Type derivedType, string typeDiscriminator)]
attribute constructors to include the C# namespaces, as nameof(type)
does not include the C# namespace.
Demo fiddle #2 here.