@JsonTypeInfo でポリモーフィックな JSON をモデリングする
Jackson の @JsonTypeInfo と @JsonSubTypes で、"type" 識別子フィールドを持つ JSON を正しい具象サブクラスにデシリアライズします。
Special Cases
詳細な説明
JSON におけるポリモーフィズム
具体的な形が識別子フィールドに依存するオブジェクトを返す API があります。Jackson の @JsonTypeInfo と @JsonSubTypes を使えば、ひとつの JSON を複数の Java サブクラスのどれかにマッピングできます。
JSON の例
{
"type": "credit_card",
"last4": "4242",
"brand": "visa"
}
{
"type": "bank_account",
"account_number": "****6789",
"routing_number": "021000021"
}
Java 階層
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 {
// 共通フィールド(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;
}
自動生成での扱い
単一の JSON ドキュメントだけではポリモーフィズムは判別できないため、自動ジェネレーターは具象クラスを 1 つ生成するに留まります。生成後は次の手順で整理します。
- 識別子フィールドを特定する(よくあるのは
type、kind、object) - 共通フィールドを抽象基底クラスに切り出す
- バリアント固有のフィールドをサブクラスへ移す
@JsonTypeInfoと@JsonSubTypesを付与する
他の識別子戦略
include = As.WRAPPER_OBJECT— 型名をキーとする外側オブジェクトでラップするinclude = As.EXISTING_PROPERTY— 各サブクラスにすでに存在するプロパティを識別子として利用use = JsonTypeInfo.Id.CLASS— 完全修飾 Java クラス名を埋め込む(公開 API では避ける)
Sealed 階層(Java 17+)
Java 17 では sealed クラスにより、誰が抽象型を継承できるかを制限できます。
public sealed abstract class PaymentMethod permits CreditCard, BankAccount {}
@JsonSubTypes と組み合わせると、消費側で網羅的なパターンマッチが可能です。
String summary = switch (method) {
case CreditCard cc -> "Card ending " + cc.getLast4();
case BankAccount ba -> "Bank " + ba.getRoutingNumber();
};
ユースケース
決済 API(Stripe の payment_method.type)、イベントストリーム(CloudEvents の type フィールド)、Webhook エンベロープ(event_type)、通知システムなどはポリモーフィックなペイロードを返します。Jackson の型階層としてモデリングすれば消費側の instanceof チェーンを排除できます。