Search code examples
c#jsonpolymorphismsystem.text.json.net-7.0

How can I serialize a multi-level polymorphic type hierarchy with System.Text.Json in .NET 7?


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?


Solution

  • 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).

    Demo fiddle