はじめに
外部 API のレスポンスを Jackson で DTO に変換していると、相手側に項目が追加された途端に UnrecognizedPropertyException が発生することがあります。
この問題への対処として、よく使われるのが次の 2 方式です。
// ObjectMapper に設定する方式
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// DTO に設定する方式
@JsonIgnoreProperties(ignoreUnknown = true)
どちらも未定義の JSON キーを読み飛ばすため、一見すると同じ設定に見えます。実際、単一の DTO 直下にある未定義キーだけを扱うなら、デシリアライズ結果は同じです。
ただし、設定が及ぶ範囲は異なります。この記事では、再現テストの結果をもとに、2 方式の違いと使い分けを整理します。
未定義キーを許容する設定は、外部 API の項目追加に対する互換性を得るためのものです。JSON の型不一致や必須項目不足まで許容する設定ではありません。
先に結論
単一 DTO の直下にある未定義キーを読み飛ばすだけなら、2 方式の結果は同じです。どちらも既知のフィールドを復元し、未定義キーを無視します。
差が出るのは適用範囲です。
| 観点 | FAIL_ON_UNKNOWN_PROPERTIES = false |
@JsonIgnoreProperties(ignoreUnknown = true) |
|---|---|---|
| 設定する場所 | ObjectMapper |
DTO クラス |
| 効く範囲 | その Mapper で読む DTO 全体 | 注釈を付けた DTO |
| 未注釈のネスト DTO | 未定義キーを読み飛ばす | 未定義キーで例外になる |
| 向いている場面 | 専用 Mapper で読む API 群を一律に許容する | 許容対象を API・DTO 単位に限定する |
| 共有 Mapper での注意点 | 影響範囲が広がりやすい | 対象を追いやすい |
アプリケーション全体で共有する ObjectMapper を使っているなら、まず DTO 単位の注釈を検討するのが安全です。専用の ObjectMapper で特定の外部 API を読む構成なら、Mapper 側の設定も自然な選択になります。
デフォルトでは未定義キーは例外になる
Jackson の FAIL_ON_UNKNOWN_PROPERTIES はデフォルトで有効です。JSON のキーが DTO のプロパティへ対応付かず、@JsonAnySetter やハンドラでも処理されない場合、具体的には UnrecognizedPropertyException(JsonMappingException のサブクラス)を送出します。DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
例えば、DTO に存在しない externalAddedField がレスポンスへ追加されたケースを考えます。
{
"resultCd": "0",
"message": "success",
"externalAddedField": "new-value"
}
public class ApiResponse {
private String resultCd;
private String message;
// getter / setter は省略
}
デフォルト設定の ObjectMapper で読み込むと例外になります。
ObjectMapper objectMapper = new ObjectMapper();
ApiResponse response = objectMapper.readValue(json, ApiResponse.class);
// UnrecognizedPropertyException: Unrecognized field "externalAddedField"
この挙動は、想定外の API 変更を早く検知できるという点では有用です。一方、後方互換な項目追加だけで連携が止まることを避けたい場合には、未定義キーの扱いを明示的に決める必要があります。
方式1:ObjectMapper で未定義キーを許容する
ObjectMapper に FAIL_ON_UNKNOWN_PROPERTIES = false を設定すると、その Mapper がデシリアライズする未定義キーを読み飛ばせます。
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
ApiResponse response = objectMapper.readValue(json, ApiResponse.class);
この方式では、同じ ObjectMapper を通るすべての DTO が設定の影響を受けます。ルート DTO だけでなく、ネストした未注釈の DTO も対象です。
設定の作用範囲が広いため、外部 API ごとに Mapper を分けている場合は扱いやすくなります。
public class PartnerApiClient {
private final ObjectMapper partnerApiMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
public PartnerResponse getResponse(String json) throws JsonProcessingException {
return partnerApiMapper.readValue(json, PartnerResponse.class);
}
}
外部連携が多いシステムでは、連携先が独立してレスポンスへ項目を追加することがあります。既存項目の意味や型が変わらず、自システムで利用しない項目が追加されただけなら、未知フィールドを読み飛ばして処理を継続する方が、連携全体を停止させずに済みます。
これは、未知フィールドの追加には寛容にしつつ、既知フィールドの破壊的な変更には厳格にする考え方です。例えば amount が文字列からオブジェクトへ変わる変更は、未知フィールドの追加ではなく契約変更です。FAIL_ON_UNKNOWN_PROPERTIES = false はこのような型変更まで許容する設定ではありません。
連携先が多数あり、外部 API のレスポンス群に同じ方針を適用したい場合は、外部 API 専用の ObjectMapper を用意して一律に未定義キーを許容する構成も選択肢になります。社内 JSON や内部 API には厳格な Mapper を使い、外部連携だけを専用 Mapper に分けることで、許容範囲をシステムの境界に合わせて管理できます。
ただし、Spring Boot などで共有している ObjectMapper にこの設定を入れると、意図しない API や内部データまで未定義キーを許容することがあります。共有 Mapper のデフォルト設定を変更するときは、利用箇所を確認する必要があります。
方式2:DTO に @JsonIgnoreProperties を付ける
@JsonIgnoreProperties(ignoreUnknown = true) を DTO に付けると、その DTO が受け取る未定義キーを例外にせず読み飛ばせます。JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
public class PartnerResponse {
private String resultCd;
private String message;
// getter / setter は省略
}
この方式の利点は、どのレスポンスで未定義キーを許容しているかを DTO の定義から追いやすいことです。
外部 API のレスポンス DTO にだけ注釈を付ければ、内部 API や設定ファイルのデシリアライズまで一律に緩くする必要がありません。
@JsonIgnoreProperties は特定のキーだけを無視する用途にも使えます。
@JsonIgnoreProperties({"legacyField"})
public class PartnerResponse {
private String resultCd;
// getter / setter は省略
}
この指定は legacyField だけを無視します。将来追加される任意のキーまで許容する ignoreUnknown = true とは用途が異なります。
ルート DTO だけなら、2 方式の結果は同じ
次の JSON を、Mapper 設定ありの DTO と注釈付き DTO へそれぞれ読み込むテストを作成しました。
{
"resultCd": "0",
"message": "success",
"unrecognizedKey": "added-by-external-system"
}
テストで確認した結果は次のとおりです。
| 条件 | 結果 |
|---|---|
デフォルトの ObjectMapper
|
unrecognizedKey で UnrecognizedPropertyException
|
FAIL_ON_UNKNOWN_PROPERTIES = false |
成功。resultCd と message を復元 |
@JsonIgnoreProperties(ignoreUnknown = true) |
成功。resultCd と message を復元 |
この範囲では、読み飛ばしの可否と既知フィールドの復元結果に差はありません。
差はネストした DTO で表れる
実務で重要なのは、レスポンスがネストしている場合です。ルート DTO だけに注釈を付けた例を見ます。
@JsonIgnoreProperties(ignoreUnknown = true)
public class OrderResponse {
private String resultCd;
private OrderDetail detail;
// getter / setter は省略
}
public class OrderDetail {
private String id;
// getter / setter は省略
}
次の JSON には、ルートとネスト先の両方に未定義キーがあります。
{
"resultCd": "0",
"unknownAtRoot": "root-value",
"detail": {
"id": "A-001",
"unknownAtNested": "nested-value"
}
}
OrderResponse の注釈は unknownAtRoot には効きます。しかし、OrderDetail には注釈がないため、unknownAtNested で UnrecognizedPropertyException になります。
一方、FAIL_ON_UNKNOWN_PROPERTIES = false を設定した ObjectMapper で同じ JSON を読むと、ルートとネスト先の未定義キーの両方を読み飛ばします。
この違いを、次のように捉えると分かりやすくなります。
| 設定 | ルート DTO の未定義キー | 未注釈のネスト DTO の未定義キー |
|---|---|---|
| Mapper 設定 | 読み飛ばす | 読み飛ばす |
ルート DTO のみの @JsonIgnoreProperties
|
読み飛ばす | 例外になる |
ルート・ネスト DTO の両方に @JsonIgnoreProperties
|
読み飛ばす | 読み飛ばす |
DTO 単位の注釈を選ぶ場合は、ネストした DTO にも同じ方針が必要かを確認します。外部 API が大きく、ネスト先が多い場合は、注釈の付け忘れがテストで分かるようにしておくと安心です。
使い分けの基準
2 方式は優劣ではなく、許容範囲をどこで管理するかの違いです。次の基準で選ぶと判断しやすくなります。
専用 Mapper で外部 API 全体を許容したい場合
特定の外部 API 専用に ObjectMapper を用意しており、その API のレスポンス群を一律に許容したい場合は、Mapper 設定が向いています。
- 外部 API 専用のクライアントと Mapper がある
- レスポンス DTO 全体で、後方互換な項目追加を許容する方針である
- ネストした DTO も含めて同じ方針にしたい
この場合、設定の適用範囲と API の境界が一致します。
API・DTO ごとに許容範囲を限定したい場合
同じ ObjectMapper を複数の用途で共有している場合や、一部の API だけ項目追加を許容したい場合は、DTO の注釈が向いています。
- 特定の外部 API のレスポンスだけを許容したい
- 内部 API や設定ファイルでは、未知のキーを検知したい
- どの DTO が緩い契約を持つかをコード上で明示したい
この場合は、必要なネスト DTO にも注釈を付けるか、ネスト先だけは厳格に扱うかを設計として決めます。
仕様変更を確実に検知したい場合
外部 API の変更を検知して対応したい場合や、契約の厳密さを優先する場合は、どちらの設定も入れずデフォルトの例外動作を維持します。
例えば、金額、権限、状態遷移のように、項目追加自体が業務上の確認対象になる API では、無条件に読み飛ばすより早期に失敗させる方が安全なことがあります。
未定義キーを読み飛ばすときの注意点
未定義キーの許容は、連携停止を避けるための有効な手段です。しかし、相手側の仕様変更を見えなくする側面もあります。
運用では、次のような組み合わせを検討します。
- 本番処理では未定義キーを許容して可用性を保つ
- 受信した JSON をマスキングしたうえでログに残し、項目追加を観測できるようにする
- ステージングや契約テストでは厳格な Mapper を使い、仕様変更を検知する
- API 提供元の変更通知や OpenAPI 定義の差分確認を運用に組み込む
単に例外を消すのではなく、許容する変更と検知したい変更を分けることが重要です。
再現テスト
この記事の結論は、Java 17、Jackson Databind 2.17.2、JUnit Jupiter 5.10.3 で実行した 6 件のテストに基づいています。テストでは、トップレベルの未定義キー、Mapper 設定、クラス注釈、未注釈のネスト DTO を比較しました。
再現用コードは jackson-unknown-property-behavior にあります。次のコマンドでテストを実行できます。
mvn test
まとめ
FAIL_ON_UNKNOWN_PROPERTIES = false と @JsonIgnoreProperties(ignoreUnknown = true) は、単一 DTO の直下にある未定義キーを読み飛ばすという点では同じです。
使い分けるときは、設定の適用範囲を基準にします。
- 外部 API 専用の Mapper でレスポンス群を一律に許容するなら、Mapper 設定を使う
- 許容範囲を API・DTO 単位に絞るなら、
@JsonIgnoreProperties(ignoreUnknown = true)を使う - 仕様変更を検知したいなら、デフォルトの例外動作を維持する
- DTO 注釈を使う場合は、ネスト DTO にも同じ方針を適用するかを決める
設定を選ぶ前に、どの API のどの変更を許容し、どの変更を検知したいかを整理すると、意図せず契約を緩めることを避けられます。