java-recipes

ホーム ファイルI/O › F-04: JSON の読み書き

F-04: JSON の読み書き(Jackson)

REST API やファイルで JSON を扱う場面は現代の Java 開発で頻繁に出てきます。 Pure Java には標準の JSON ライブラリがないため、実務では Jackson や Gson などの外部ライブラリを使うのが一般的です。 ここでは最も普及している Jackson を使ったパターンを解説します。

Jackson の基本概念

Jackson はの主要な処理方法は2つです。

データバインディング(POJO / record との相互変換)

Java オブジェクトと JSON を直接変換する方法です。クラス定義が必要ですが、型安全に扱えます。writeValueAsString() でシリアライズ、readValue() でデシリアライズします。

ツリーモデル(JsonNode)

JSON をツリー構造として扱う方法です。クラス定義なしで任意のフィールドにアクセスできます。 スキーマが不定な JSON や、一部のフィールドだけ取り出したい場合に便利です。

Maven 依存定義

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.0</version>
</dependency>

サンプルコード

Sample.java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

public class JsonSample {

    // ① Jackson 用 POJO(getter/setter が必須)
    static class Person {
        private String name;
        private int age;

        // デフォルトコンストラクタ:Jackson がデシリアライズ時に内部で使用するため必須
        public Person() {}

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public int getAge() { return age; }
        public void setAge(int age) { this.age = age; }

        @Override
        public String toString() {
            return "Person{name='" + name + "', age=" + age + "}";
        }
    }

    // ObjectMapper は生成コストが高いため static final で1つだけ作成する
    // メソッドごとに new ObjectMapper() するのはNG
    private static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {

        // ② Java オブジェクト → JSON 文字列(シリアライズ)
        Person person = new Person("山田太郎", 30);
        String json = MAPPER.writeValueAsString(person);
        System.out.println("シリアライズ: " + json);
        // → {"name":"山田太郎","age":30}

        // ③ JSON 文字列 → Java オブジェクト(デシリアライズ)
        String input = "{\"name\":\"鈴木花子\",\"age\":25}";
        Person parsed = MAPPER.readValue(input, Person.class);
        System.out.println("デシリアライズ: " + parsed);

        // ④ 整形出力(pretty print)
        String pretty = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(person);
        System.out.println("整形出力:\n" + pretty);

        // ⑤ ネストした JSON を JsonNode(ツリー構造)で汎用的に扱う
        String nested = "{\"id\":1,\"address\":{\"city\":\"Tokyo\",\"zip\":\"100-0001\"}}";
        JsonNode root = MAPPER.readTree(nested);
        System.out.println("id: " + root.get("id").asInt());
        System.out.println("city: " + root.path("address").path("city").asText());
        // path() は存在しないキーでも NullPointerException が発生しない(get() は null を返す)

        // ⑥ 配列 JSON → オブジェクト配列
        String arrayJson = "[{\"name\":\"田中\",\"age\":20},{\"name\":\"佐藤\",\"age\":35}]";
        Person[] people = MAPPER.readValue(arrayJson, Person[].class);
        for (Person p : people) {
            System.out.println("配列要素: " + p);
        }

        // ⑦ オブジェクトリスト → JSON
        List<Person> list = Arrays.asList(new Person("高橋", 28), new Person("伊藤", 32));
        String listJson = MAPPER.writeValueAsString(list);
        System.out.println("リスト JSON: " + listJson);
    }
}

よくあるミス・注意点

⚠️ ObjectMapper は new するたびに重い処理が走る

ObjectMapper はスレッドセーフかつ生成コストが高いオブジェクトです。 メソッドを呼ぶたびに new ObjectMapper() すると、 大量のリクエストを処理するアプリでは深刻なパフォーマンス問題になります。 必ず static final で1度だけ生成して使い回しましょう。

// NG: メソッドを呼ぶたびに新規作成(パフォーマンスが悪い)
public String toJson(Object obj) throws Exception {
    return new ObjectMapper().writeValueAsString(obj);
}

// OK: static final で1つだけ生成
private static final ObjectMapper MAPPER = new ObjectMapper();

public String toJson(Object obj) throws Exception {
    return MAPPER.writeValueAsString(obj);
}

📌 POJO にはデフォルトコンストラクタが必要

Jackson はデシリアライズ時にデフォルトコンストラクタ(引数なしのコンストラクタ)を使ってオブジェクトを生成します。 引数付きのコンストラクタしか定義していないクラスに readValue() するとInvalidDefinitionException が発生します。

📌 未知フィールドがあるとデシリアライズが失敗する

デフォルトでは JSON に POJO にないフィールドがあると UnrecognizedPropertyException が発生します。 API レスポンスが変わっても壊れないようにするには @JsonIgnoreProperties(ignoreUnknown = true) を付けましょう。

テストする観点

  • シリアライズしてデシリアライズすると元のオブジェクトと一致するか(ラウンドトリップテスト)
  • JSON に未知フィールドが含まれているとき、@JsonIgnoreProperties なしで例外が発生するか
  • 不正な JSON 文字列を渡すと JsonParseException がスローされるか
  • 日本語(マルチバイト文字)を含む値が文字化けなく扱えるか
  • 配列 JSON → オブジェクト配列への変換が正しく機能するか
  • JsonNode.path() で存在しないキーにアクセスしても例外が発生しないか

GitHub でソースコードを見る →