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?

2 Answers 2

4

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.

Sign up to request clarification or add additional context in comments.

Comments

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

2 Comments

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.
@dbc 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)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.