I have some classes that I want to (de-)serialize:
public class Top
{
public Top(Sub content) { Content = content; }
public Sub Content { get; init; }
}
public class Sub
{
public Sub(Sub? entry) { Entry = entry; Type = SubType.super; }
public Sub? Entry { get; init; }
public SubType Type { get; init; }
}
public class SubA : Sub
{
public SubA(Sub? entry) : base(entry) { Type = SubType.a; }
}
public enum SubType { super, a }
Example object:
var top = new Top(new SubA(new Sub(new SubA(null))));
To serialize, I just need to use JsonSerializer.Serialize
with some options to get what I want:
var json = JsonSerializer.Serialize(top, new JsonSerializerOptions
{ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { _enumConverter } });
// result:
// {"Content":{"Entry":{"Entry":{"Type":"a"},"Type":"super"},"Type":"a"}}
Deserializing does not work out of the box - it always deserializes to Sub
, never to SubA
. So I tried writing my own JsonConverter
that finds the type T
to deserialize to (from the JSON Type
property), then calls the appropriate JsonSerializer.Deserialize<T>
method. But I end up either in a StackOverflow or in losing my converter after one level:
public class SubConverter : JsonConverter<Sub>
{
public override BaseType Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
// Create a copy of the reader to find type. Necessary because after type was found, we
// need to deserialize from the start, but resetting to start position is not possible.
var typeReader = reader;
bool discriminatorFound = false;
while (typeReader.Read())
{
if (typeReader.TokenType == JsonTokenType.StartObject
|| typeReader.TokenType == JsonTokenType.StartArray)
{
typeReader.Skip();
continue;
}
if (typeReader.TokenType != JsonTokenType.PropertyName)
continue;
if (typeReader.GetString() != TypeDiscriminatorPropertyName)
continue;
discriminatorFound = true;
break;
}
if (!discriminatorFound)
throw new JsonException(
$"type discriminator property \"{TypeDiscriminatorPropertyName}\" was not found");
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.String)
throw new JsonException("type discriminator value does not exist or is not a string");
var typeString = typeReader.GetString();
var deserializationType = typeString == SubType.super.ToString() ? typeof(Sub) : typeof(SubA);
// !!!
// if THIS, is not removed, will get infinite loop (-> StackOverflowException)
// if THIS is removed, will not get polymorphic deserialization in properties below
var options2 = new JsonSerializerOptions(options);
if (options2.Converters.Contains(this))
options2.Converters.Remove(this);
BaseType inst = (BaseType)JsonSerializer.Deserialize(ref reader, deserializationType, options2)!;
return inst;
}
// not needed; we only use this converter for deserialization
public override void Write(Utf8JsonWriter writer, BaseType value, JsonSerializerOptions options)
{ throw new NotImplementedException(); }
}
If I just pass the options
unchanged into JsonSerializer.Deserialize
, I will get an inifinite loop (JsonSerializer.Deserialize
will call SubConverter.Read
and vice versa).
If I remove the SubConverter
from the options as in the code above, I lose it for all content in the levels below. So instead of the original object foo
that was like this:
Top -> SubA -> Sub -> SubA
I now get
Top -> SubA -> Sub -> Sub
^ ^
│ └─ because `SubConverter` was removed, cannot deserialize as SubA
└─ here we removed `SubConverter`
What do I do now?
I do not want to write the whole deserialization on my own, only the necessary bit(s). (My real use case is much more complex than the classes in this question.)