Reputation: 1825
.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
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
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
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