I have a multi-level polymorphic type hierarchy that I previously serialized using the data contract serializers. I would like to convert that to System.Text.Json using the new type hierarchy support in .NET 7. Where should I apply the [JsonDerivedType]
attributes so that "grandchild" and other deeply derived subtypes of subtypes can be serialized correctly?
My original type hierarchy looked like this:
[KnownType(typeof(DerivedType))]
public abstract class BaseType { } // Properties omitted
[KnownType(typeof(DerivedOfDerivedType))]
public class DerivedType : BaseType { public string DerivedValue { get; set; } }
public class DerivedOfDerivedType : DerivedType { public string DerivedOfDerivedValue { get; set; } }
I replaced the [KnownType]
attributes with [JsonDerivedType]
attributes as follows:
[JsonDerivedType(typeof(DerivedType), "DerivedType:#MyNamespace")]
public abstract class BaseType { } // Properties omitted
[JsonDerivedType(typeof(DerivedOfDerivedType), "DerivedOfDerivedType:#MyNamespace")]
public class DerivedType : BaseType { public string DerivedValue { get; set; } }
public class DerivedOfDerivedType : DerivedType { public string DerivedOfDerivedValue { get; set; } }
However when I serialize as List<BaseType>
as follows:
var list = new List<BaseType> { new DerivedOfDerivedType { DerivedValue = "value 1", DerivedOfDerivedValue = "value of DerivedOfDerived" } };
var json = JsonSerializer.Serialize(list);
I get the following exception:
System.NotSupportedException: Runtime type 'MyNamespace.DerivedOfDerivedType' is not supported by polymorphic type 'MyNamespace.BaseType'. Path: $.
---> System.NotSupportedException: Runtime type 'MyNamespace.DerivedOfDerivedType' is not supported by polymorphic type 'MyNamespace.BaseType'.
Where should the JsonDerivedType
attributes be applied to make this work?
I've dabbled with the same task and wrote some POC custom contact resolver which applies all JsonDerivedTypeAttribute
's from the hierarchy to the root:
static void AddNestedDerivedTypes(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.PolymorphismOptions is null) return;
var derivedTypes = jsonTypeInfo.PolymorphismOptions.DerivedTypes
.Where(t => Attribute.IsDefined(t.DerivedType, typeof(JsonDerivedTypeAttribute)))
.Select(t => t.DerivedType)
.ToList();
var hashset = new HashSet<Type>(derivedTypes);
var queue = new Queue<Type>(derivedTypes);
while (queue.TryDequeue(out var derived))
{
if (!hashset.Contains(derived))
{
// Todo: handle discriminators
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(derived, derived.FullName));
hashset.Add(derived);
}
var attribute = derived.GetCustomAttributes<JsonDerivedTypeAttribute>();
foreach (var jsonDerivedTypeAttribute in attribute) queue.Enqueue(jsonDerivedTypeAttribute.DerivedType);
}
}
Which can be set up in the options:
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { AddNestedDerivedTypes }
}
};
SomeRootType container = ...;
var json = JsonSerializer.Serialize(container, options);
var typedToBase = JsonSerializer.Deserialize<SomeRootType>(json, options);
Obviously implementation is far from perfect and requires a lot of refining both feature- and performance-wise (supporting discriminators from the attributes, possibly caching type infos, maybe even using source generators).