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

2

3

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?

Vincenty answered 28/11, 2022 at 17:45 Comment(0)
T
2

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

Twandatwang answered 28/11, 2022 at 19:38 Comment(2)
A couple suggestions: 1) replace t.DerivedType.GetCustomAttribute<JsonDerivedTypeAttribute>() with Attribute.IsDefined(). If multiple attributes are applied, the former will throw. 2) Add derived to the hash set in case somebody manually added a derived type to multiple base types. See dotnetfiddle.net/l0hFBS.Vincenty
@Vincenty thank you, fixed the attributes handling (the hashset part was already added to the answer, just was not added to the fiddle, which for some reason is not updating)Twandatwang
V
3

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 type DerivedType in order to serialize values that are declared to be DerivedType, 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.

Vincenty answered 28/11, 2022 at 17:45 Comment(0)
T
2

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

Twandatwang answered 28/11, 2022 at 19:38 Comment(2)
A couple suggestions: 1) replace t.DerivedType.GetCustomAttribute<JsonDerivedTypeAttribute>() with Attribute.IsDefined(). If multiple attributes are applied, the former will throw. 2) Add derived to the hash set in case somebody manually added a derived type to multiple base types. See dotnetfiddle.net/l0hFBS.Vincenty
@Vincenty thank you, fixed the attributes handling (the hashset part was already added to the answer, just was not added to the fiddle, which for some reason is not updating)Twandatwang

© 2022 - 2024 — McMap. All rights reserved.