シナリオ
REST APIを呼び出すか、JSONファイルを読み込んでいます。JacksonがレスポンスをDTOにマッピングし始めると、次のエラーが発生します:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "fieldName" (class com.example.MyDto), not marked as ignorable (4 known properties: ...)
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1001)
見覚えがありますか?APIが新しいフィールド(例:"createdAt")を追加したのに、DTOにはそのフィールドがなく、Jacksonが処理を拒否します。あるいは、JavaクラスでフィールドをOr you renamed userName から displayName に変更したのに、JSONはまだ古いキーを送信している場合も同様です。いずれの場合も、同じエラーが発生します。
Jacksonがこのエラーをスローする理由
JacksonのObjectMapperは設計上厳格です。JSONにターゲットクラスのフィールドと一致しないプロパティが含まれている場合、データを暗黙的に無視するのではなく、即座に失敗します。その意図は合理的で、スキーマの不一致を早期に検出し、後続の処理で微妙なバグが発生するのを防ぎます。
実際には、このエラーはいくつかの予測可能な状況で発生します:
- サードパーティAPIが新しいフィールドを追加したが、DTOをまだ更新していない
- JSONが異なるスキーマを持つアプリの古いバージョンから送信されている
- 時間の経過とともに乖離したサービス間でDTOを共有している
- フィールド名のどこかにタイポがある
クイックフィックス:DTOクラスにアノテーションを付ける
クラスに@JsonIgnoreProperties(ignoreUnknown = true)を付けます。Jacksonはマッピングできないすべてのフィールドをスキップします。例外も発生せず、実際に必要なフィールドのデータも失われません:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MyDto {
private String name;
private int age;
// getters and setters
}
この修正は局所的です。MyDtoだけが寛容になり、コードベースの他の部分は厳格なままです。自社の内部DTOは信頼しつつ、外部APIからの予測不能なレスポンスに対応する必要がある場合、これが適切なトレードオフです。
グローバルフィックス:ObjectMapperを設定する
すべての箇所で寛容な動作が必要ですか?ObjectMapperを一度設定し、そのインスタンスを全体で使用します:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Use this mapper for all deserialization
MyDto dto = mapper.readValue(jsonString, MyDto.class);
重要なポイント:一度設定してインスタンスを共有することです。呼び出しごとに新しいObjectMapperを作成するのはよくある間違いで、コストがかかり、設定フラグを見落としやすくなります。
Spring Boot:グローバルなObjectMapper設定
Spring Bootは自動設定を通じて独自のObjectMapperを管理しています。コントローラーのデシリアライゼーション用に手動で作成する必要はありません(すべきでもありません)。application.propertiesに以下を追加します:
spring.jackson.deserialization.fail-on-unknown-properties=false
またはapplication.ymlの場合:
spring:
jackson:
deserialization:
fail-on-unknown-properties: false
Spring Bootはこの設定を自動的に読み込みます。すべてのコントローラー、RestTemplateの呼び出し、WebClientのレスポンスで使用される共有ObjectMapperに適用されます。
カスタムObjectMapperビーンを定義している場合は、そこにも同じフラグを適用してください。そうしないと、プロパティファイルの設定がカスタムインスタンスに影響しません:
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
不明なフィールドを実際にキャプチャしたい場合
不明なフィールドを無視することが常に正しい選択とは限りません。「認識されない」フィールドに必要なデータが含まれている場合もあります。
**方法1:フィールドを明示的に追加する。**JSONがuser_nameを送信しているのにJavaフィールドがuserNameの場合、@JsonPropertyで命名のギャップを埋めます:
import com.fasterxml.jackson.annotation.JsonProperty;
public class MyDto {
@JsonProperty("user_name")
private String userName;
}
**方法2:Mapですべてをキャッチする。**スキーマが本当に動的な場合(WebフックのペイロードやプラグインのAPIなど)、@JsonAnySetterを使ってマッピングされていないフィールドをMapに収集します:
import com.fasterxml.jackson.annotation.JsonAnySetter;
import java.util.HashMap;
import java.util.Map;
public class MyDto {
private String name;
private Map<String, Object> extra = new HashMap<>();
@JsonAnySetter
public void setExtra(String key, Object value) {
extra.put(key, value);
}
}
クラスで宣言されていないフィールドはすべてextraに入ります。何もドロップされません。
検証
完了と判断する前に、焦点を絞ったユニットテストで修正を確認します:
@Test
void shouldDeserializeWithExtraFields() throws Exception {
String json = "{\"name\": \"Alice\", \"age\": 30, \"unknownField\": \"ignored\"}";
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MyDto dto = mapper.readValue(json, MyDto.class);
assertEquals("Alice", dto.getName());
assertEquals(30, dto.getAge());
// No exception — fix confirmed
}
修正後もまだ例外が発生していますか?最も一般的な原因は、呼び出しチェーンのどこかに別のObjectMapperインスタンスが存在することです。設定フラグなしで作成されたものです。コードベースでnew ObjectMapper()を検索し、それぞれを確認してください。
ヒント
実際にJSONを読める状態にすると、デシリアライゼーションの失敗のデバッグがずっと楽になります。レスポンスがminify済み(空白のない文字の羅列)の場合は、ToolCraftのJSON Formatter & Validatorに貼り付けると即座に展開できます。800バイトのminified JSONで"user_name"と"userName"の違いを見つけるのは苦痛ですが、整形されたツリー表示なら2秒で見つかります。
また、選択肢がある場合はグローバル設定よりもクラスごとのアノテーションを優先することをお勧めします。特定のDTOに@JsonIgnoreProperties(ignoreUnknown = true)を付けることは明示的で、将来コードを読む人がそのクラスが意図的に寛容であることを理解できます。グローバルにチェックを無効にするのは便利ですが、実際のスキーマのずれを隠してしまう可能性があります。グローバルフラグは、サードパーティAPI・レガシー連携など、受信するJSONの形状を本当に制御できない場合にのみ使用してください。

