Polymorphic output in the Management API

How to support polymorphic outputs from custom Management APIs

For security reasons, the System.Text.Json serializer will not serialize types that are not explicitly referenced at compile time.

This can be a challenge when dealing with polymorphic API outputs. As a workaround, the Management API provides two options for enabling polymorphic outputs.

Polymorphism by interface

This approach requires that all output models implement the same interface - for example:

IMyItem.cs
public interface IMyItem
{
    Guid Id { get; }

    string Value { get; set; }
}
MyItem.cs
public class MyItem(string value) : IMyItem
{
    public Guid Id { get; } = Guid.NewGuid();

    public string Value { get; set; } = value;
}
MyOtherItem.cs
public class MyOtherItem(string value, int otherValue) : IMyItem
{
    public Guid Id { get; } = Guid.NewGuid();

    public string Value { get; set; } = value;

    public int OtherValue { get; } = otherValue;
}

The ProducesResponseType annotation on the endpoints must also be updated to use the interface:

MyItemApiController.cs
...
[ProducesResponseType<PagedViewModel<IMyItem>>(StatusCodes.Status200OK)]
public IActionResult GetAllItems(int skip = 0, int take = 10)
...
[ProducesResponseType<IMyItem>(StatusCodes.Status200OK)]
public IActionResult GetItem(Guid id)
...

Polymorphism by annotation

This approach requires that all output models implement a common base class. The base class will define all its derived types by annotation - for example:

MyItemBase.cs
[JsonDerivedType(typeof(MyItem), nameof(MyItem))]
[JsonDerivedType(typeof(MyOtherItem), nameof(MyOtherItem))]
public abstract class MyItemBase(string value)
{
    public Guid Id { get; } = Guid.NewGuid();

    public string Value { get; set; } = value;
}
MyItem.cs
public class MyItem(string value) : MyItemBase(value)
{
}
MyOtherItem.cs
public class MyOtherItem(string value, int otherValue) : MyItemBase(value)
{
    public int OtherValue { get; } = otherValue;
}

The ProducesResponseType annotation on the endpoints must also be updated to use the base class:

MyItemApiController.cs
...
[ProducesResponseType<PagedViewModel<MyItemBase>>(StatusCodes.Status200OK)]
public IActionResult GetAllItems(int skip = 0, int take = 10)
...
[ProducesResponseType<MyItemBase>(StatusCodes.Status200OK)]
public IActionResult GetItem(Guid id)
...

Last updated