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.

LuTiao Programming
LuTiao Programming
LuTiao Programming
How to Elegantly Handle Dynamic Request Bodies in Spring Boot

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootREST APIjacksonRequestBodyDynamic JSONJsonNodePolymorphic Deserialization
LuTiao Programming
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.