Model Polymorphic JSON with @JsonTypeInfo
Use Jackson's @JsonTypeInfo and @JsonSubTypes to deserialize JSON that includes a "type" discriminator field into the correct concrete subclass.
Detailed Explanation
Polymorphism in JSON
Some APIs return objects whose concrete shape depends on a discriminator field. Jackson's @JsonTypeInfo and @JsonSubTypes annotations let you map a single JSON document to one of several Java subclasses.
Example JSON
{
"type": "credit_card",
"last4": "4242",
"brand": "visa"
}
{
"type": "bank_account",
"account_number": "****6789",
"routing_number": "021000021"
}
Java Hierarchy
import com.fasterxml.jackson.annotation.*;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = CreditCard.class, name = "credit_card"),
@JsonSubTypes.Type(value = BankAccount.class, name = "bank_account")
})
public abstract class PaymentMethod {
// common fields, e.g., id, customer_id
}
public class CreditCard extends PaymentMethod {
private String last4;
private String brand;
}
public class BankAccount extends PaymentMethod {
@JsonProperty("account_number")
private String accountNumber;
@JsonProperty("routing_number")
private String routingNumber;
}
How the Generator Handles This
The auto-generator emits a single concrete class per input JSON because a single document does not reveal the polymorphism. After generation:
- Identify the discriminator field (often
type,kind, orobject) - Extract the common fields into an abstract base class
- Move the variant-specific fields into subclasses
- Add
@JsonTypeInfoand@JsonSubTypesannotations
Other Discriminator Strategies
include = As.WRAPPER_OBJECT— wrap the payload in an outer object whose key is the type nameinclude = As.EXISTING_PROPERTY— use a discriminator field that already exists on every subclassuse = JsonTypeInfo.Id.CLASS— encode the fully qualified Java class name (avoid for public APIs)
Sealed Hierarchies (Java 17+)
Java 17 added sealed classes, which restrict who can extend an abstract type:
public sealed abstract class PaymentMethod permits CreditCard, BankAccount {}
Combined with @JsonSubTypes, sealed classes give you exhaustive pattern matching in the consumer:
String summary = switch (method) {
case CreditCard cc -> "Card ending " + cc.getLast4();
case BankAccount ba -> "Bank " + ba.getRoutingNumber();
};
Use Case
Payment APIs (Stripe payment_method.type), event streams (CloudEvents type field), webhook envelopes (event_type), and notification systems all return polymorphic payloads. Modeling them as a Jackson type hierarchy prevents instanceof chains in the consumer.