With .NET 7.0 being released, System.Text.Json
is supposed to support polymorphic code. Unfortunately, it seems like it can't be used out of the box when you need to return a derived type's instance from a controller's method. For instance, suppose the following model:
public class Base {}
public class Derived1: Base { }
public class Derived2: Base { }
Suppose also that we've got the following dynamic type info resolver:
public class JsonHierarchyTypeInfoResolver : DefaultJsonTypeInfoResolver {
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) {
var jsonTypeInfo = base.GetTypeInfo(type, options);
if( typeof(Base) == jsonTypeInfo.Type ) {
jsonTypeInfo.PolymorphismOptions = new( ) {
TypeDiscriminatorPropertyName = "$type",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
DerivedTypes = {
new JsonDerivedType(typeof(Derived1), typeof(Derived1).AssemblyQualifiedName!),
new JsonDerivedType(typeof(Derived2), typeof(Derived2).AssemblyQualifiedName!)
}
};
}
return jsonTypeInfo;
}
}
And then, it's used in the app by doing something like this:
builder.Services.AddControllers( )
.AddJsonOptions(options => {
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.TypeInfoResolver = new JsonHierarchyTypeInfoResolver( );
});
Let's also assume that we've got a simple controller with a simple method that looks like this:
[ApiController]
[Route("[controller]")]
public class DEMOController : ControllerBase {
[HttpGet]
public ActionResult<Base> GetAsync() {
var derived = new Derived1( );
return Ok(derived);
}
}
Whenever the method is called, it will not generate json with the type descriminator as I expected it to. It seems like the problem lies with the SystemTextJsonOutputFormatter
when it tries to serialize the object with the following code:
await JsonSerializer.SerializeAsync(responseStream, context.Object, objectType, SerializerOptions, httpContext.RequestAborted);
and here's how objectType
is initialized:
// context.ObjectType reflects the declared model type when specified.
// For polymorphic scenarios where the user declares a return type, but returns a derived type,
// we want to serialize all the properties on the derived type. This keeps parity with
// the behavior you get when the user does not declare the return type and with Json.Net at least at the top level.
var objectType = context.Object?.GetType() ?? context.ObjectType ?? typeof(object);
Since the method uses the derived type, the custom info type resolver won't be able to do its magic. Am I missing something? Is this a known issue? Does this mean that I should keep using json.net instead of trying to migrate to System.Text.Json?
The type which ASP.NET Core resolves for GetAsync
would be Derived1
which does not have any polymorphism info, so it will not add any polymorphic serialization data. One solution is to add polymorphism info for every type in the hierarchy:
public class JsonHierarchyTypeInfoResolver : DefaultJsonTypeInfoResolver
{
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
var jsonTypeInfo = base.GetTypeInfo(type, options);
// if type inherits or is Base
if (jsonTypeInfo.Type.IsAssignableTo(typeof(Base)))
{
jsonTypeInfo.PolymorphismOptions = new()
{
TypeDiscriminatorPropertyName = "$type",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
};
var derivedTypes = new[]
{
typeof(WeatherForecastController.Derived1),
typeof(WeatherForecastController.Derived2)
}
.Where(t => jsonTypeInfo.Type.IsAssignableTo(t)) // add only appropriate types
.Select(t => new JsonDerivedType(t, t.AssemblyQualifiedName!));
foreach (var derivedType in derivedTypes)
{
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(derivedType);
}
}
return jsonTypeInfo;
}
}