概要

外部 API 連携が本番稼働して数ヶ月、先方がレスポンスにフィールドを1つ追加した途端に取込が全件エラーになる――Jackson の UnrecognizedPropertyException は、この形で障害になりやすい例外です。Java 標準 API には JSON パーサーが含まれないため、多くのプロジェクトでは Jackson の ObjectMapper を使うことになります。readValue / writeValueAsString の基本は簡単ですが、record との組み合わせ、未知フィールドへの備え、ネスト構造を辿るときの get() と path() の違いなど、判断の要るポイントが散在しています。この記事ではシリアライズ・デシリアライズの基本から、フィールドが増えたとき実際に何が起きるかの失敗例と @JsonIgnoreProperties による回避までを、動くコードで整理します。

使いどころ

外部 API のレスポンス JSON を Java オブジェクトにマッピングして業務ロジックで使う

DB から取得したデータを JSON 形式に変換してフロントエンドに返す

設定ファイルやテストデータを JSON 形式で管理し、起動時やテスト時に読み込む

コード例

Jackson による 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));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record(Java 16+)で不変なデータクラスとしてマッピングできる。テキストブロックで JSON リテラルを読みやすく記述できる。

Java 17
// 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

JacksonJava の JSON ライブラリのデファクト。Spring Boot にもバンドルされており、多くのプロジェクトで標準的に使われる。依存サイズがやや大きい。設定項目が多く、初回学習コストがある。
Gson軽量な JSON ライブラリが欲しい場合。アノテーションなしでも動作する。record サポートは追加設定が必要。大規模プロジェクトでは Jackson のほうが機能が豊富。
JSON-P / JSON-B(Jakarta EE)Jakarta EE 環境で標準仕様に準拠したい場合。スタンドアロンで使うには依存の追加が必要。Jackson ほどのエコシステムはない。

注意点

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

ObjectMapper を毎回 new してはいけないのですか。

インスタンス生成コストが高いため、static final で1つだけ作成してください。ObjectMapper はスレッドセーフです。

record クラスで Jackson を使うにはどうしますか。

Jackson 2.12 以降で record をサポートしています。@JsonProperty でフィールド名を明示するのが確実です。

ネスト構造の JSON で特定の値だけ取りたい場合は。

ObjectMapper.readTree() で JsonNode を取得し、path("key").path("nested").asText() のようにチェーンで辿れます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます