概要
外部 API 連携が本番稼働して数ヶ月、先方がレスポンスにフィールドを1つ追加した途端に取込が全件エラーになる――Jackson の UnrecognizedPropertyException は、この形で障害になりやすい例外です。Java 標準 API には JSON パーサーが含まれないため、多くのプロジェクトでは Jackson の ObjectMapper を使うことになります。readValue / writeValueAsString の基本は簡単ですが、record との組み合わせ、未知フィールドへの備え、ネスト構造を辿るときの get() と path() の違いなど、判断の要るポイントが散在しています。この記事ではシリアライズ・デシリアライズの基本から、フィールドが増えたとき実際に何が起きるかの失敗例と @JsonIgnoreProperties による回避までを、動くコードで整理します。
使いどころ
外部 API のレスポンス JSON を Java オブジェクトにマッピングして業務ロジックで使う
DB から取得したデータを JSON 形式に変換してフロントエンドに返す
設定ファイルやテストデータを JSON 形式で管理し、起動時やテスト時に読み込む
コード例
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import java.util.List;
public class JsonParsing {
record Person(
@JsonProperty("name") String name,
@JsonProperty("age") int age
) {}
// 外部 API のレスポンス用: 知らないフィールドが増えても壊れない
@JsonIgnoreProperties(ignoreUnknown = true)
record TolerantPerson(
@JsonProperty("name") String name,
@JsonProperty("age") int age
) {}
private static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
var person = new Person("山田太郎", 30);
var json = MAPPER.writeValueAsString(person);
System.out.println("JSON: " + json); // JSON: {"name":"山田太郎","age":30}
var input = """
{"name":"鈴木花子","age":25}
""";
var parsed = MAPPER.readValue(input, Person.class);
System.out.println("オブジェクト: " + parsed);
// 失敗例: 先方がフィールドを追加した後のレスポンス
var extended = """
{"name":"高橋","age":40,"dept":"開発"}
""";
try {
MAPPER.readValue(extended, Person.class);
} catch (UnrecognizedPropertyException e) {
System.out.println("未知フィールドで失敗: " + e.getPropertyName()); // 未知フィールドで失敗: dept
}
// ignoreUnknown = true の型なら同じ JSON がそのまま通る
System.out.println("許容版: " + MAPPER.readValue(extended, TolerantPerson.class));
var nested = """
{"id":1,"address":{"city":"Tokyo","zip":"100-0001"}}
""";
JsonNode root = MAPPER.readTree(nested);
System.out.println("city: " + root.path("address").path("city").asText());
// 存在しないキーでも path() なら MissingNode で受け流せる
System.out.println("country: " + root.path("address").path("country").asText("N/A")); // country: N/A
// root.get("address").get("country").asText() と書くと get("country") が null を返し NPE
var arrayJson = """
[{"name":"田中","age":20},{"name":"佐藤","age":35}]
""";
var people = List.of(MAPPER.readValue(arrayJson, Person[].class));
people.forEach(p -> System.out.println(" " + p));
}
}Version Coverage
record(Java 16+)で不変なデータクラスとしてマッピングできる。テキストブロックで JSON リテラルを読みやすく記述できる。
// Java 17: record で簡潔にマッピング
record Person(
@JsonProperty("name") String name,
@JsonProperty("age") int age
) {}
var person = MAPPER.readValue(json, Person.class);
var input = """
{"name":"山田","age":30}
""";Library Comparison
注意点
ObjectMapper はスレッドセーフだがインスタンス生成コストが高い。static final で1つだけ作成し、メソッドごとに new しないこと。
デシリアライズ対象クラスにはデフォルトコンストラクタが必要(POJO の場合)。record の場合は @JsonProperty でフィールド名を明示する。
未知のフィールドがあると UnrecognizedPropertyException が発生する。@JsonIgnoreProperties(ignoreUnknown = true) で回避可能。API レスポンスを受ける場合は設定推奨。
JsonNode.get() は存在しないキーで null を返すため NPE のリスクがある。path() を使えば MissingNode が返り安全。
実務では外部 API の仕様変更でフィールドが増減したときに UnrecognizedPropertyException が発生して障害になるケースがある。外部 API のレスポンスを受けるクラスには @JsonIgnoreProperties(ignoreUnknown = true) を最初から付けておくのが安全。
FAQ
インスタンス生成コストが高いため、static final で1つだけ作成してください。ObjectMapper はスレッドセーフです。
Jackson 2.12 以降で record をサポートしています。@JsonProperty でフィールド名を明示するのが確実です。
ObjectMapper.readTree() で JsonNode を取得し、path("key").path("nested").asText() のようにチェーンで辿れます。