One endpoint, multiple payloads: Handling polymorphic request bodies in Spring Boot
One endpoint, multiple payloads: Handling polymorphic request bodies in Spring Boot with Java sealed interfaces
A clean and type-safe approach to managing dynamic JSON payloads using Jackson polymorphism and modern Java features.

In the development of REST APIs, it is common to encounter scenarios where a single endpoint must be able to accept JSON payloads with different structures. For example, an ordering system might receive both online orders and in-store orders, each with its own specific data.
This tutorial explores a solution for handling such dynamic request bodies using Java Sealed Interfaces and Jackson’s polymorphism features within a Spring Boot application.
The Problem: One Endpoint, Multiple Structures
Let’s imagine a POST /orders endpoint. This endpoint must be able to create an order, whether it is:
- An online order, identified by the customer’s email and delivery address.
- An in-store order, identified by the store ID and a flag indicating express preparation.
Instead of creating two separate endpoints (/orders/online and /orders/store), we want to keep a single entry point to simplify the API contract.
The Solution: Polymorphism with Jackson
The key to our solution lies in how we instruct Jackson to deserialize incoming JSON into the correct Java object. To achieve this, we use a sealed interface that serves as the base contract for all order types.
The Contract: the Command Sealed Interface
We define a Command interface. The sealed keyword ensures that only the classes declared in permits can implement it, which strengthens the robustness of our domain model.
Using Jackson annotations, we enable polymorphic deserialization:
- @JsonTypeInfo: Indicates that type information is included directly in the JSON, via a property named type.
- @JsonSubTypes: Defines the mapping between type values (e.g. “online”) and concrete Java classes (e.g. OnlineCommand.class).
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OnlineCommand.class, name = "online"),
@JsonSubTypes.Type(value = StoreCommand.class, name = "store")
})
public sealed interface Command
permits OnlineCommand, StoreCommand {
}
Concrete Implementations
Our two order types are implemented as Java records, providing a concise and expressive syntax for immutable data objects.
public record OnlineCommand(
String email,
String deliveryAddress
) implements Command {
}
public record StoreCommand(
String storeId,
boolean express
) implements Command {
}
The Entry Point: OrderController
The controller becomes remarkably simple. The createOrder endpoint expects a Command as its @RequestBody. Spring Boot and Jackson handle the deserialization, and we don’t need to worry about the concrete type at this level.
SpringDoc annotations (@Operation, etc.) are used to generate clear API documentation that accurately reflects the polymorphic nature of the expected request body.
@Operation(summary = "Create a new order, which can be an online order or a store order.",
description = "The type of order is determined by the 'type' field in the request body. " +
"Use 'online' for online orders and 'store' for in-store orders.")
@PostMapping
public ResponseEntity<Long> createOrder(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Order details, which vary based on the order type.",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(
oneOf = {OnlineCommand.class, StoreCommand.class},
discriminatorProperty = "type",
discriminatorMapping = {
@DiscriminatorMapping(value = "online", schema = OnlineCommand.class),
@DiscriminatorMapping(value = "store", schema = StoreCommand.class)
}
)
)
)
@RequestBody Command command) {
Long id = service.createOrder(command);
return ResponseEntity.ok(id);
}

Business Logic: OrderService and Pattern Matching
The real “magic” happens in the service layer. Thanks to sealed interfaces, we can use Java’s switch pattern matching. This is a modern, safe, and readable way to handle different cases without relying on verbose if/else chains and instanceof checks.
Not only is the code cleaner, but the compiler can also verify that all possible cases (OnlineCommand and StoreCommand) are handled—no need for a default branch.
public Long createOrder(Command command) {
OrderEntity entity = new OrderEntity();
switch (command) {
case OnlineCommand online -> {
logger.info("Receiving an online order");
entity.setType("online");
entity.setPayload(serialize(online));
}
case StoreCommand store -> {
logger.info("Receiving an in-store order");
entity.setType("store");
entity.setPayload(serialize(store));
}
}
return repository.save(entity).getId();
}
Usage Examples
Here’s how you can call the API with both types of payloads.
curl -X POST http://localhost:8080/orders
-H "Content-Type: application/json"
-d '{
"type": "online",
"email": "[email protected]",
"deliveryAddress": "123 Main St, Anytown"
}'
curl -X POST http://localhost:8080/orders
-H "Content-Type: application/json"
-d '{
"type": "store",
"storeId": "STORE-456",
"express": true
}'
Conclusion
By combining the power of Java sealed interfaces for modeling and Jackson polymorphism annotations, we have built a robust, clean, and maintainable API capable of handling complex data structures through a single endpoint.
One endpoint, multiple payloads: Handling polymorphic request bodies in Spring Boot was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.
This post first appeared on Read More

