Master Polymorphic Deserialization in Spring Boot 3 with Jackson
This tutorial shows how to use Jackson's polymorphic deserialization in Spring Boot 3 to handle variable JSON request bodies by defining a common Order interface, concrete implementations, and a REST controller that routes calculations based on the order type, eliminating redundant endpoints.
Introduction
When the JSON body format is unknown, defining a separate API for each possible structure leads to redundant endpoints. Polymorphic deserialization with Jackson provides a concise and extensible solution.
Case Study
Assume an online store needs to handle three order types: regular, discount, and promo, each with different fields and calculation logic.
1. Interface Definition
<code>@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "orderType", visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = RegularOrder.class, name = "regular"),
@JsonSubTypes.Type(value = DiscountOrder.class, name = "discount"),
@JsonSubTypes.Type(value = PromoOrder.class, name = "promo")
})
public interface Order {
}</code>2. POJO Implementations
<code>public class DiscountOrder implements Order {
private double price;
private int quantity;
private double discountRate;
}
public class PromoOrder implements Order {
private double price;
private int quantity;
private String promoCode;
}
public class RegularOrder implements Order {
private double price;
private int quantity;
}</code>3. Controller
<code>@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/calculate")
public ResponseEntity<Double> calculateTotal(@RequestBody Order order) {
double total = 0;
switch (order) {
case RegularOrder r -> total = orderService.calculateRegularTotal(r);
case DiscountOrder d -> total = orderService.calculateDiscountTotal(d);
case PromoOrder p -> total = orderService.calculatePromoTotal(p);
default -> logger.error("Unknown order type: {}", order);
}
return ResponseEntity.ok(total);
}
}</code>4. Service
<code>@Service
public class OrderService {
public double calculateRegularTotal(RegularOrder order) {
return order.getPrice() * order.getQuantity();
}
public double calculateDiscountTotal(DiscountOrder order) {
return order.getPrice() * order.getQuantity() * (1 - order.getDiscountRate());
}
public double calculatePromoTotal(PromoOrder order) {
return order.getPrice() * order.getQuantity() * 0.75;
}
}</code>5. Alternative: Dynamic Field Types
By annotating a field with @JsonTypeInfo and @JsonSubTypes, the field can hold different Java types based on a "type" property in the JSON.
<code>public class Order {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = String.class, name = "string"),
@JsonSubTypes.Type(value = Long.class, name = "long"),
@JsonSubTypes.Type(value = Integer.class, name = "int")
})
protected Object value;
}</code>When the request contains
"type":"long", the
valuefield is deserialized as a
Long, etc.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.