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?
CodePudding user response:
The [JsonDerivedType]
attribute must be applied to every base type (other than System.Object
) that might be declared for serialization.
Thus [JsonDerivedType(typeof(DerivedOfDerivedType), "DerivedOfDerivedType:#MyNamespace")]
must be duplicated on BaseType
and DerivedType
like so:
// Derived types of BaseType
[JsonDerivedType(typeof(DerivedType), "DerivedType:#MyNamespace")]
// Derived types of DerivedType copied from DerivedType
[JsonDerivedType(typeof(DerivedOfDerivedType), "DerivedOfDerivedType:#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; } }
Notes:
The
[JsonDerivedType]
attribute must be applied to the intermediate typeDerivedType
in order to serialize values that are declared to beDerivedType
, e.g.:var list = new List<DerivedType> { new DerivedOfDerivedType { DerivedValue = "value 1", DerivedOfDerivedValue = "value of DerivedOfDerived" } }; var json = JsonSerializer.Serialize(list);
If intermediate types in the polymorphic type hierarchy are never serialized independently,
[JsonDerivedType]
need only be applied only to the root base type.While the data contract serializers and
XmlSerializer
will automatically discover known types of known types recursively during serialization of a base type, it seems that this feature was omitted from System.Text.Json. Thus the application must do this manually by copying[JsonDerivedType]
attributes onto all relevant base types, or alternatively writing some custom contract resolver that propagates the derived types to base types automatically.
Demo fiddle here.
CodePudding user response:
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:
private static void AddNestedDerivedTypes(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.PolymorphismOptions is null)
{
return;
}
var derivedTypes = jsonTypeInfo.PolymorphismOptions.DerivedTypes
.Where(t => t.DerivedType.GetCustomAttribute<JsonDerivedTypeAttribute>() != null)
.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));
}
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).