Handling Different Payload Types in ASP.NET Core with a Custom Input Formatter

In modern APIs, especially integrations with third-party platforms, it’s common to receive JSON payloads where a field like "command" determines the structure of the "record" or "data" part of the payload.

There are a few common options for handling this, one of which uses ASP.NET Core custom input formatters. In this post, I’ll show how I built a custom input formatter in ASP.NET Core that reads a discriminator field (like "command") and deserializes the request into different .NET types accordingly.


Example Scenario

I receive POST requests with a structure similar to this:

{
  "command": "openObject",
  "record": {
    "data": {
      "attributes": {
        "name": "2",
        "original_identifier": "45678"
      }
    }
  }
}

The shape of "attributes" depends on the "command". I wanted to deserialize the "attributes" into different C# classes like:

public class OpenObjectPayload
{
    public string Name { get; set; }
    public string OriginalIdentifier { get; set; }
}

public class CloseObjectPayload
{
    public string DoorName { get; set; }
    public DateTime ClosedAt { get; set; }
}

Solution: Custom Input Formatter

I used this to inspect the incoming JSON and route it to the correct class.

1. Create a Base Class

public abstract class WebhookRequest
{
    public string Command { get; set; }
}

2. Define Derived Payload Classes

public class OpenObjectRequest : WebhookRequest
{
    public OpenObjectPayload Record { get; set; }
}

public class CloseObjectRequest : WebhookRequest
{
    public CloseObjectPayload Record { get; set; }
}

3. Create the Custom Input Formatter

public class CommandBasedInputFormatter : TextInputFormatter
{
    private readonly Dictionary<string, Type> _typeMappings;

    public CommandBasedInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
        SupportedEncodings.Add(Encoding.UTF8);

        _typeMappings = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
        {
            { "openObject", typeof(OpenObjectRequest) },
            { "closeObject", typeof(CloseObjectRequest) }
        };
    }

    protected override bool CanReadType(Type type) => type == typeof(WebhookRequest);

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        var request = context.HttpContext.Request;
        using var reader = new StreamReader(request.Body, encoding);
        var json = await reader.ReadToEndAsync();

        var command = JsonDocument.Parse(json).RootElement.GetProperty("command").GetString();

        if (!_typeMappings.TryGetValue(command, out var targetType))
        {
            context.ModelState.AddModelError("command", $"Unknown command: {command}");
            return await InputFormatterResult.FailureAsync();
        }

        var result = JsonSerializer.Deserialize(json, targetType, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

        return await InputFormatterResult.SuccessAsync(result);
    }
}

Register the Formatter

In Startup.cs (for .NET Core 3.1) or Program.cs (for .NET 6+):

services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new CommandBasedInputFormatter());
});

Use in Controller

[HttpPost("webhook")]
public IActionResult HandleWebhook([FromBody] WebhookRequest request)
{
    switch (request)
    {
        case OpenObjectRequest openObject:
            // Handle open object
            break;
        case CloseObjectRequest closeObject:
            // Handle close object
            break;
        default:
            return BadRequest("Unknown command");
    }

    return Ok();
}

Benefits

Centralised logic for routing based on discriminator fields
Strongly typed models per command
Compatible with application/json and standard model binding


Conclusion

Using a custom input formatter in ASP.NET Core is a clean and powerful way to handle polymorphic request bodies driven by a command or type field. This approach keeps the controller actions strongly typed and easier to maintain.