MarengoHue
MarengoHue

Reputation: 1825

JSON Polymorphic serialization in .NET7 Web API

.NET7 Includes a lot of improvements for System.Text.Json serializer, one of which is polymorphic serialization of types using the new [JsonPolymorphic] attribute. I am trying to use it in my Asp.Net web API, however it doesn't seem to serialize the type discriminator despite the fact that the model is properly setup.

It only happens when trying to send the objects over the wire, when using JsonSerializer, everything appears to be working well. For example:

// This is my data model
[JsonPolymorphic]
[JsonDerivedType(typeof(SuccesfulResult), typeDiscriminator: "ok")]
[JsonDerivedType(typeof(ErrorResult), typeDiscriminator: "fail")]
public abstract record Result;
public record SuccesfulResult : Result;
public record ErrorResult(string Error) : Result;
// Some test code that actually works
var testData = new Result[]
{
    new SuccesfulResult(),
    new ErrorResult("Whoops...")
};

var serialized = JsonSerializer.SerializeToDocument(testData);
// Serialized string looks like this:
// [{ "$type": "ok" }, { "$type": "fail", "error": "Whoops..." }]
// So type discriminators are in place and all is well
var deserialized = serialized.Deserialize<IEnumerable<Result>>()!;

// This assertion passes succesfully!
// We deserialized a collection polymorphically and didn't lose any type information.
testData.ShouldDeepEqual(deserialized);
// However, when sending the object as a response in an API, the AspNet serializer
// seems to be ignoring the attributes:

[HttpGet("ok")]
public Result GetSuccesfulResult() => new SuccesfulResult();

[HttpGet("fail")]
public Result GetFailResult() => new ErrorResult("Whoops...");

Neither of these responses are annotated with a type discriminator and my strongly-typed clients can't deserialize the results into a proper hierarchy.

GET /api/ok HTTP/1.1
# =>
# ACTUAL: {}
# EXPECTED: { "$type": "ok" }
GET /api/fail HTTP/1.1
# =>
# ACTUAL: { "error": "Whoops..." }
# EXPECTED: { "$type": "fail", "error": "Whoops..." }

Am I missing some sort of API configuration that would make controllers serialize results in a polymorphic manner?

Upvotes: 5

Views: 5275

Answers (3)

Patrick Hovsepian
Patrick Hovsepian

Reputation: 175

I ran into the same issue. All the proper polymorphic attributes were in place for the base class. I ended up working around it by explicitly invoking the serializer myself Results.Content(JsonSerializer.Serialize(data, opts.Value), "application/json")

Note: It worked as expected and serialized the $type for IEnumerable<BaseType> via Results.Ok

Upvotes: 0

user19620212
user19620212

Reputation:

You have to specify the base type when serializing for it to work without annotating every derived type with [JsonDerivedType(...)]

var serialized = JsonSerializer.SerializeToDocument<Result[]>(testData);

Update:

This is a known bug in ASP.NET Core tracked here and here. (Doesn't affect IEnumerable results)

The link has a workaround by a .NET Team member in this comment using a custom resolver as follows



    var options = new JsonSerializerOptions { TypeInfoResolver = new InheritedPolymorphismResolver() };
    JsonSerializer.Serialize(new Derived(), options); // {"$type":"derived","Y":0,"X":0}
    
    [JsonDerivedType(typeof(Base), typeDiscriminator: "base")]
    [JsonDerivedType(typeof(Derived), typeDiscriminator: "derived")]
    public class Base
    {
        public int X { get; set; }
    }
    
    public class Derived : Base
    {
        public int Y { get; set; }
    }
    
    public class InheritedPolymorphismResolver : DefaultJsonTypeInfoResolver
    {
        public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
        {
            JsonTypeInfo typeInfo = base.GetTypeInfo(type, options);
    
            // Only handles class hierarchies -- interface hierarchies left out intentionally here
            if (!type.IsSealed && typeInfo.PolymorphismOptions is null && type.BaseType != null)
            {
                // recursively resolve metadata for the base type and extract any derived type declarations that overlap with the current type
                JsonPolymorphismOptions? basePolymorphismOptions = GetTypeInfo(type.BaseType, options).PolymorphismOptions;
                if (basePolymorphismOptions != null)
                {
                    foreach (JsonDerivedType derivedType in basePolymorphismOptions.DerivedTypes)
                    {
                        if (type.IsAssignableFrom(derivedType.DerivedType))
                        {
                            typeInfo.PolymorphismOptions ??= new()
                            {
                                IgnoreUnrecognizedTypeDiscriminators = basePolymorphismOptions.IgnoreUnrecognizedTypeDiscriminators,
                                TypeDiscriminatorPropertyName = basePolymorphismOptions.TypeDiscriminatorPropertyName,
                                UnknownDerivedTypeHandling = basePolymorphismOptions.UnknownDerivedTypeHandling,
                            };
    
                            typeInfo.PolymorphismOptions.DerivedTypes.Add(derivedType);
                        }
                    }
                }
            }
    
            return typeInfo;
        }
    }

It should be fixed in .NET 8

Upvotes: 3

MarengoHue
MarengoHue

Reputation: 1825

Specifying [JsonDerivedType(...)] on individual subclasses and on the base type seems to resolve an issue but barely seems intentional. This possibly might be fixed in future releases.

[JsonPolymorphic]
[JsonDerivedType(typeof(SuccesfulResult), typeDiscriminator: "ok")]
[JsonDerivedType(typeof(ErrorResult), typeDiscriminator: "fail")]
public abstract record Result;

[JsonDerivedType(typeof(SuccesfulResult), typeDiscriminator: "ok")]
public record SuccesfulResult : Result;

[JsonDerivedType(typeof(ErrorResult), typeDiscriminator: "fail")]
public record ErrorResult(string Error) : Result;

Upvotes: 10

Related Questions