How to Elegantly Handle Dynamic Request Bodies in Spring Boot
This article explains why using a fixed DTO for a constantly changing /orders endpoint is problematic, compares simple Map and JsonNode approaches, and presents a robust polymorphic @JsonTypeInfo solution that provides type safety, IDE support, and easy extensibility.
When an API endpoint such as /orders receives JSON payloads with completely different structures—physical product orders, digital downloads, or subscription services—developers often resort to repeatedly adding new DTO classes, changing controller signatures, or altering database schemas, which leads to maintenance headaches and frequent front‑end complaints.
What is a Request Body?
In a POST or PUT request, the client sends JSON in the HTTP body, which Spring Boot can bind to a Java object using @RequestBody. By default Jackson deserializes the JSON, mapping field names to Java properties. This works well when the JSON structure is fixed.
Problem: One Endpoint, Multiple Structures
Three example order payloads illustrate the issue:
{
"type": "physical",
"productName": "Laptop",
"weight": 2.5,
"shippingAddress": "California"
} {
"type": "digital",
"productName": "E-Book",
"downloadUrl": "http://example.com/download"
} {
"type": "subscription",
"planName": "Pro Plan",
"durationMonths": 12
}All share the same URL but have different fields, making a single static DTO unsuitable.
Solution 1: Use a Map<String, Object> (Quick Fix)
@PostMapping("/orders")
public String createOrder(@RequestBody Map<String, Object> body) {
String type = (String) body.get("type");
if ("physical".equals(type)) {
return "Processing physical order";
} else if ("digital".equals(type)) {
return "Processing digital order";
} else if ("subscription".equals(type)) {
return "Processing subscription order";
}
return "Unknown order type";
}Advantages : flexible, no need for multiple DTO classes.
Disadvantages : no compile‑time type safety, hard to maintain, IDE cannot offer field completion.
This approach is acceptable for temporary prototypes but not for long‑term projects.
Solution 2: Use JsonNode (More Structured)
@PostMapping("/orders")
public String createOrder(@RequestBody JsonNode node) {
String type = node.get("type").asText();
switch (type) {
case "physical":
double weight = node.get("weight").asDouble();
return "Physical order weight: " + weight;
case "digital":
String url = node.get("downloadUrl").asText();
return "Digital download: " + url;
case "subscription":
int duration = node.get("durationMonths").asInt();
return "Subscription for: " + duration + " months";
default:
return "Unknown order type";
}
}Advantages : safer than a raw map, supports nested JSON.
Disadvantages : still requires manual field extraction, business logic remains tightly coupled to JSON structure.
Ultimate Solution: Polymorphic Deserialization with @JsonTypeInfo
Define an abstract parent class annotated with Jackson’s polymorphic metadata, then create concrete subclasses for each order type.
package com.icoderoad.order.dto;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = PhysicalOrderRequest.class, name = "physical"),
@JsonSubTypes.Type(value = DigitalOrderRequest.class, name = "digital"),
@JsonSubTypes.Type(value = SubscriptionOrderRequest.class, name = "subscription")
})
public abstract class OrderRequest {
private String type;
public String getType() { return type; }
}Concrete subclasses contain only the fields relevant to their type:
public class PhysicalOrderRequest extends OrderRequest {
private String productName;
private double weight;
private String shippingAddress;
// getters
}
public class DigitalOrderRequest extends OrderRequest {
private String productName;
private String downloadUrl;
// getters
}
public class SubscriptionOrderRequest extends OrderRequest {
private String planName;
private int durationMonths;
// getters
}The controller now accepts the abstract type and relies on instanceof checks (or pattern matching) to dispatch logic:
@RestController
@RequestMapping("/orders")
public class OrderController {
@PostMapping
public String createOrder(@RequestBody OrderRequest request) {
if (request instanceof PhysicalOrderRequest physical) {
return "Shipping physical product: " + physical.getProductName();
}
if (request instanceof DigitalOrderRequest digital) {
return "Providing download link: " + digital.getDownloadUrl();
}
if (request instanceof SubscriptionOrderRequest sub) {
return "Activating subscription: " + sub.getPlanName();
}
return "Unsupported order type";
}
}Why this is the best approach :
Compile‑time type safety.
IDE auto‑completion for fields.
Adding a new order type only requires a new subclass and a registration entry in @JsonSubTypes; the controller remains unchanged.
Follows the Open/Closed Principle, keeping the codebase clean and maintainable.
Extending the system is straightforward. For a new giftcard order, create GiftCardOrderRequest with its fields and add it to the @JsonSubTypes list; no controller modification is needed.
Takeaway
Frequent DTO changes are a low‑level workaround. Using a raw Map is a temporary measure, and manual JsonNode parsing still couples business logic to JSON. Leveraging Jackson’s polymorphic deserialization lets the framework handle dispatching, lets the type system enforce correctness, and keeps the controller stable even as the request schema evolves.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
