Java Tips | 基本ユーティリティ:JSONパース

Java Java
スポンサーリンク

JSONパースは「外の世界のデータを自分の型に落とす」技

JSON文字列化が「Javaのオブジェクトを外の世界に出す」技だとしたら、
JSONパースはその逆で、「外の世界から来た JSON を、自分の型に安全に落とし込む」技です。

ここを雑にやると、型が合わない・必須項目が抜けている・想定外の値が来て落ちる、など、
本番でしか起きないイヤなバグの温床になります。
だからこそ、「JSONパース用のユーティリティ」を一つ決めておくと、コード全体がかなり落ち着きます。


前提:文字列化ユーティリティと同じ ObjectMapper を使い回す

「JSON文字列化」と「JSONパース」は同じエンジンで動かす

JSON文字列化のときに使った Jackson の ObjectMapper は、パースでもそのまま使えます。
ここをバラバラにすると、「出すときと読むときでルールが違う」という最悪パターンになるので、
同じ設定の ObjectMapper を、文字列化とパースで共有するのが鉄則です。

例えば、前回の Jsons を少し拡張して、パースも扱えるようにします。

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public final class Jsons {

    private static final ObjectMapper MAPPER = createMapper();

    private Jsons() {}

    private static ObjectMapper createMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return mapper;
    }

    public static String toJson(Object value) {
        try {
            return MAPPER.writeValueAsString(value);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException("JSON 文字列化に失敗しました: " + value, e);
        }
    }

    public static <T> T fromJson(String json, Class<T> type) {
        try {
            return MAPPER.readValue(json, type);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("JSON パースに失敗しました: " + json, e);
        }
    }
}
Java

ここでの重要ポイントは、fromJson「JSON文字列+欲しい型」から、型付きのオブジェクトを返す窓口になっていることです。
呼び出し側は ObjectMapper を意識せず、「JSON をこの型にしてほしい」とだけ指定すればよくなります。


基本:JSON → POJO のパース

フィールド名が合っていれば、そのままマッピングされる

まずは、素直な POJO にパースする例からいきます。

public class User {

    private String id;
    private String name;
    private int age;

    public User() {}

    public String getId()   { return id; }
    public String getName() { return name; }
    public int getAge()     { return age; }

    public void setId(String id)     { this.id = id; }
    public void setName(String name) { this.name = name; }
    public void setAge(int age)      { this.age = age; }
}
Java

この JSON をパースしてみます。

{"id":"u-001","name":"山田太郎","age":30}

コードはこうなります。

String json = """
        {"id":"u-001","name":"山田太郎","age":30}
        """;

User user = Jsons.fromJson(json, User.class);

System.out.println(user.getId());   // u-001
System.out.println(user.getName()); // 山田太郎
System.out.println(user.getAge());  // 30
Java

ここで深掘りしたいのは、「JSON のキー名と、Java のフィールド(または getter/setter)の名前が一致していれば、自動でマッピングされる」ことです。
特別なアノテーションを付けなくても、基本的な POJO であればそのままパースできます。

逆に言うと、「JSON 側が user_id、Java 側が userId」のように名前が違う場合は、
アノテーション(@JsonProperty("user_id"))などで対応する必要があります。


例題:部分的な JSON を Map で受ける

「全部の型をきっちり決めなくてもいい」場面

外部サービスから返ってくる JSON が大きくて、
「とりあえず status と message だけ見たい」というような場面では、
POJO を定義せずに Map で受けるのもよくあるパターンです。

import java.util.Map;

String json = """
        {"status":"ok","message":"done","extra":{"debug":true}}
        """;

Map<?, ?> map = Jsons.fromJson(json, Map.class);

System.out.println(map.get("status"));  // ok
System.out.println(map.get("message")); // done
System.out.println(map.get("extra"));   // {debug=true} など
Java

ここでのポイントは、「Map で受けると楽だが、型安全性は落ちる」ことです。
map.get("status") の戻り値は Object なので、キャストミスやキー名のタイプミスに気づきにくくなります。

「一時的なデバッグ」「ログ用」「小さな JSON をざっくり見る」くらいなら Map でも十分ですが、
本番ロジックでガッツリ使うなら、素直に POJO を定義したほうが安全です。


例題:配列JSONを List にパースする

「ユーザー一覧」などの典型パターン

次は、配列になっている JSON をパースしてみます。

[
  {"id":"u-001","name":"山田太郎","age":30},
  {"id":"u-002","name":"佐藤花子","age":25}
]

これを List<User> にしたいのですが、Class<T> だけでは「List の中身の型」まで表現できません。
そこで Jackson の TypeReference を使います。

import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;

public final class Jsons {

    // さっきの内容にメソッドを追加

    public static <T> T fromJson(String json, TypeReference<T> typeRef) {
        try {
            return MAPPER.readValue(json, typeRef);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("JSON パースに失敗しました: " + json, e);
        }
    }
}
Java

使う側はこうです。

String json = """
        [
          {"id":"u-001","name":"山田太郎","age":30},
          {"id":"u-002","name":"佐藤花子","age":25}
        ]
        """;

List<User> users = Jsons.fromJson(json, new TypeReference<List<User>>() {});

System.out.println(users.size());          // 2
System.out.println(users.get(0).getName()); // 山田太郎
Java

ここでの重要ポイントは、「ジェネリクスを含む型(List<User> など)は、TypeReference で表現する」ことです。
Class<List> だけだと「中身の型」が分からないので、Jackson が List<Map> のように解釈してしまいます。
new TypeReference<List<User>>() {} という少し不思議な書き方は、
「List<User> という具体的な型情報を、ランタイムまで持ち込むためのおまじない」と覚えておくとよいです。


エラー処理とバリデーションの考え方

「パースに失敗したらどうするか」を最初に決める

JSONパースで必ず向き合うのが、「壊れた JSON が来たときどうするか」です。

fromJson の中では、JsonProcessingExceptionIllegalArgumentException にラップして投げ直しました。
これは、「JSON が想定どおりでないのは“入力の問題”なので、呼び出し側でちゃんと扱ってほしい」というメッセージでもあります。

呼び出し側では、例えばこんな方針を決めておきます。

try {
    User user = Jsons.fromJson(requestBody, User.class);
    // 正常処理
} catch (IllegalArgumentException e) {
    // 400 Bad Request を返す、ログに JSON を出す、など
}
Java

ここで大事なのは、「パースエラーを握りつぶさない」ことです。
「とりあえず null を返す」「空のオブジェクトを返す」といった実装にすると、
「どこで壊れた JSON が来ているのか」が分からなくなり、調査が地獄になります。

パース後の「業務バリデーション」とは分けて考える

JSONパースはあくまで「構造として正しいか」「型として合っているか」を見る段階です。
「このフィールドは必須」「この値は 0 以上」といった業務ルールは、
パース後に別のバリデーション層でチェックするほうが、責務の分離としてきれいです。

つまり、

  • JSONパース:構造・型レベルのチェック
  • 業務バリデーション:意味レベルのチェック

と分けて考えると、コードの見通しがよくなります。


まとめ:JSONパースユーティリティで身につけたい感覚

JSONパースは、「文字列を手で分解する」のではなく、
「ライブラリに任せつつ、“どの型に落とし込むか”をはっきり決める」ための技です。

押さえておきたい感覚は、まず「文字列化とパースで同じ ObjectMapper を共有し、設定を一箇所に集約する」こと。
次に、「POJO には Class<T>、List や Map などのジェネリクス型には TypeReference を使う」こと。
そして、「パースエラーは握りつぶさず、“入力が不正である”というシグナルとしてきちんと扱う」ことです。

タイトルとURLをコピーしました