Polymorphic deserialisation is the technique of deserialising a JSON payload into one of several possible concrete subtypes based on information embedded in the JSON itself. It is essential whenever your API works with inheritance hierarchies — event systems, payment processors, notification pipelines, or any domain where a list of heterogeneous objects needs to round-trip through JSON. Jackson handles this cleanly with @JsonTypeInfo and @JsonSubTypes.
The Problem: Deserialising an Abstract Type
Consider an API that returns a list of payment methods. Some are credit cards, others are bank transfers. Both share a common base, but each has its own fields. Without polymorphic support, Jackson cannot decide which concrete class to instantiate when it sees a generic PaymentMethod type.
Setting Up the Hierarchy with @JsonTypeInfo and @JsonSubTypes
Annotate the base class (or interface) with @JsonTypeInfo to tell Jackson where the type discriminator lives in the JSON, and with @JsonSubTypes to register the permitted subtypes:
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, // Use a logical name as the discriminator
include = JsonTypeInfo.As.PROPERTY, // Embed it as a field in the JSON object
property = "paymentType" // The JSON key that carries the type name
)
@JsonSubTypes({
@JsonSubTypes.Type(value = CreditCardPayment.class, name = "credit_card"),
@JsonSubTypes.Type(value = BankTransferPayment.class, name = "bank_transfer")
})
public abstract class PaymentMethod {
private Long paymentId;
private double amountDue;
// Getters and setters
}
Define the concrete subtypes:
public class CreditCardPayment extends PaymentMethod {
private String cardNumberLastFour;
private String cardNetwork; // "VISA", "MASTERCARD", etc.
// Getters and setters
}
public class BankTransferPayment extends PaymentMethod {
private String bankAccountIban;
private String bankName;
// Getters and setters
}
Serialising a Mixed List
ObjectMapper mapper = new ObjectMapper();
CreditCardPayment card = new CreditCardPayment();
card.setPaymentId(1L);
card.setAmountDue(99.99);
card.setCardNumberLastFour("4242");
card.setCardNetwork("VISA");
BankTransferPayment bank = new BankTransferPayment();
bank.setPaymentId(2L);
bank.setAmountDue(250.00);
bank.setBankAccountIban("GB29NWBK60161331926819");
bank.setBankName("National Bank");
List<PaymentMethod> payments = List.of(card, bank);
String jsonOutput = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(payments);
Sample output:
[
{
"paymentType" : "credit_card",
"paymentId" : 1,
"amountDue" : 99.99,
"cardNumberLastFour" : "4242",
"cardNetwork" : "VISA"
},
{
"paymentType" : "bank_transfer",
"paymentId" : 2,
"amountDue" : 250.0,
"bankAccountIban" : "GB29NWBK60161331926819",
"bankName" : "National Bank"
}
]
Deserialising a Mixed List
String json = "["
+ "{"paymentType":"credit_card","paymentId":1,"amountDue":99.99,"
+ ""cardNumberLastFour":"4242","cardNetwork":"VISA"},"
+ "{"paymentType":"bank_transfer","paymentId":2,"amountDue":250.0,"
+ ""bankAccountIban":"GB29NWBK60161331926819","bankName":"National Bank"}"
+ "]";
List<PaymentMethod> payments = mapper.readValue(
json, new TypeReference<List<PaymentMethod>>() {}
);
for (PaymentMethod payment : payments) {
if (payment instanceof CreditCardPayment cc) {
System.out.println("Card ending: " + cc.getCardNumberLastFour());
} else if (payment instanceof BankTransferPayment bt) {
System.out.println("Bank: " + bt.getBankName());
}
}
// Output:
// Card ending: 4242
// Bank: National Bank
@JsonTypeInfo Placement Options
The include attribute of @JsonTypeInfo controls where the type discriminator appears in the JSON. The four available strategies produce very different wire formats — choosing the right one affects both readability and compatibility with external consumers.
| Include strategy | Description | Example JSON |
|---|---|---|
As.PROPERTY | Type key is a regular field inside the object | {"paymentType":"credit_card","amountDue":99} |
As.WRAPPER_OBJECT | Object wrapped in a single-key envelope | {"credit_card":{"amountDue":99}} |
As.WRAPPER_ARRAY | Type name and object wrapped in a two-element array | ["credit_card",{"amountDue":99}] |
As.EXISTING_PROPERTY | Uses an existing field; does not add a new one | {"paymentType":"credit_card","amountDue":99} |
How the Code Works
- During serialisation, Jackson looks up the concrete class in the
@JsonSubTypesregistry and writes the correspondingnamevalue into the discriminator field. - During deserialisation, Jackson reads the discriminator field first, maps the name to the registered
valueclass, then deserialises the remaining fields into an instance of that class. - Pattern matching (
instanceof CreditCardPayment cc) in the consumer loop is idiomatic Java 16+ and produces clean, safe code without explicit casting.
See Also
- Part 3: Jackson Annotations Cheat Sheet
- Part 4: Custom Serialisers and Mix-ins
- Part 6: Streaming API and JsonNode
- Part 7: Jackson Security and Best Practices
AI Prompts for Polymorphic Deserialisation
Add Type Discriminator to Class Hierarchy
I have this abstract class and its concrete subclasses: [paste hierarchy here]. Add @JsonTypeInfo and @JsonSubTypes to make the hierarchy fully serialisable and deserialisable with Jackson 3. Use a “type” property as the discriminator. Show the expected JSON for each subclass, demonstrate deserialising a mixed-type list, and include a round-trip JUnit 5 test using JsonMapper that verifies each subclass round-trips correctly.
What it does: Applies @JsonTypeInfo(use=Id.NAME, include=As.PROPERTY, property="type") to the base class and a complete @JsonSubTypes registry to every concrete subclass, then generates sample JSON for each variant and a parameterised JUnit 5 test that verifies every subtype survives a full serialise-then-deserialise round-trip.
When to use it: When designing an event-driven, command-pattern, or notification system where a base type must be dispatched to multiple concrete handlers and the exact type must survive JSON serialisation across a message bus or REST API boundary.
Compare All Four @JsonTypeInfo Strategies
Using this class hierarchy: [paste hierarchy here], show all four @JsonTypeInfo placement strategies: PROPERTY (type field inline), WRAPPER_OBJECT (type as outer key), WRAPPER_ARRAY (type as first array element), and EXISTING_PROPERTY (use a field already present in the class). For each strategy, show the annotated class, a sample serialised JSON string, and one sentence describing the trade-off — readability vs compatibility with external schemas.
What it does: Generates four annotated variants of the same hierarchy, each with its corresponding sample JSON and a trade-off summary. The side-by-side format makes it straightforward to evaluate which discriminator strategy fits your API contract without having to experiment manually.
When to use it: When choosing a type discriminator strategy for a new API and needing to see the exact wire-format impact of each option before committing. Especially useful when integrating with an existing external system that already has a fixed discriminator format you must match.
Refactor to Sealed Interface + Records
I have this @JsonTypeInfo abstract class hierarchy: [paste here]. Refactor it to use a Java sealed interface with record implementations in Java 21, compatible with Jackson 3. Replace the abstract class with a sealed interface, convert each concrete subclass to a record that implements the interface, update the @JsonTypeInfo and @JsonTypeName annotations, and confirm whether @JsonSubTypes can be removed. Show a before-and-after comparison and a working deserialisation test.
What it does: Refactors the abstract class + concrete subclass pattern to a sealed interface + permitted records pattern, updates all Jackson annotations to the Jackson 3 style, and shows whether the @JsonSubTypes registry can be dropped in favour of @JsonTypeName on each record — producing a leaner, more type-safe hierarchy with exhaustive pattern matching support.
When to use it: When modernising a legacy polymorphic Jackson model to take advantage of Java 21 sealed types and pattern matching, or when a code review flags that @JsonSubTypes is out of sync with the actual subclass set and you want the compiler rather than runtime to enforce completeness.
Conclusion
Jackson’s @JsonTypeInfo and @JsonSubTypes provide a declarative, annotation-driven solution for polymorphic JSON. The key decisions are where to embed the discriminator (As.PROPERTY is the most common and readable choice) and which discriminator strategy to use (Id.NAME for human-readable names, Id.CLASS when the fully qualified class name is acceptable). Pair them with Java pattern matching for clean, safe consumption code on the deserialisation side.