CodeGym /コース /JAVA 25 SELF /動的なデータ構造の扱い:Map, List, JsonNode

動的なデータ構造の扱い:Map, List, JsonNode

JAVA 25 SELF
レベル 46 , レッスン 3
使用可能

1. JSON を Map と List に読み込む

通常、JSON を扱うときは、その構造を事前に把握しており、Java クラスとして表現できます。しかし実際にはそれほど予測どおりにはいきません。フィールドが増減したり、ネストが変わったりします。このような場合は、汎用的なデータ構造(MapList)や JSON ツリーで操作する方がはるかに便利です。

バリエーションごとに Java のモデルを作るのは時間がかかり、壊れやすい設計になります。汎用コレクションやツリーなら、固定スキーマに縛られず、必要な部分だけを柔軟に取り出せます。

JSON オブジェクトを Map にデシリアライズ

JSON の例:

{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "active": true
}

Map へのデシリアライズ:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":123,\"name\":\"Alice\",\"email\":\"alice@example.com\",\"active\":true}";

        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> data = mapper.readValue(json, Map.class);

        System.out.println(data);
        // 出力: {id=123, name=Alice, email=alice@example.com, active=true}
    }
}

これで任意のフィールドに連想配列の要素としてアクセスできます:

System.out.println(data.get("name")); // Alice

オブジェクト配列を List にデシリアライズ

JSON 配列:

[
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]

デシリアライズ:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;

public class UsersReadExample {
    public static void main(String[] args) throws Exception {
        String json = "[{\"id\":1,\"name\":\"Alice\"},{\"id\":2,\"name\":\"Bob\"}]";

        ObjectMapper mapper = new ObjectMapper();
        List<Map<String, Object>> users = mapper.readValue(json, List.class);

        for (Map<String, Object> user : users) {
            System.out.println(user.get("name"));
        }
        // 出力:
        // Alice
        // Bob
    }
}

重要な注意点: 汎用構造に読み込むと、すべての入れ子のオブジェクトは Map に、配列は List になります。複雑な構造では、型キャストと型チェックを慎重に行う必要があります:

Object items = data.get("items");
if (items instanceof List) {
    List<?> itemList = (List<?>) items;
    // ...
}

2. JsonNode: Jackson における JSON ツリー

Map/List は扱いやすい一方で安全ではありません。型を取り違えたり、ネストを見落としたりしがちです。Jackson はより強力な手段としてクラス JsonNode を提供します。これは汎用的なツリーで、各ノードはオブジェクト、配列、値、または null のいずれかです。

JsonNode の取得

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

public class JsonNodeStart {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":123,\"name\":\"Alice\",\"tags\":[\"java\",\"json\"]}";
        ObjectMapper mapper = new ObjectMapper();

        JsonNode root = mapper.readTree(json);
        // root は JSON ツリーのルートです
    }
}

フィールドへのアクセス

int id = root.get("id").asInt();         // 123
String name = root.get("name").asText();  // Alice
JsonNode tags = root.get("tags");         // 配列

System.out.println("名前: " + name);

配列の走査

for (JsonNode tag : tags) {
    System.out.println(tag.asText());
}
// 出力:
// java
// json

入れ子のオブジェクト

複雑な JSON の例:

{
  "user": {
    "id": 1,
    "profile": {
      "nickname": "java_guru",
      "age": 25
    }
  }
}

入れ子の値の抽出:

JsonNode profile = root.get("user").get("profile");
String nickname = profile.get("nickname").asText();
System.out.println(nickname); // java_guru

安全なアクセス: getpath

- get("キー") — キーがなければ null を返します。null に対して asText() などを呼ぶと NullPointerException になります。
- path("キー") — キーがなければ「空」のノードを返し、asText()""asInt()0 を返します。

String phone = root.path("phone").asText(); // フィールドがなければ ""

ノードの型チェック

if (root.has("tags") && root.get("tags").isArray()) {
    for (JsonNode tag : root.get("tags")) {
        // ...
    }
}

JsonNode の変更

JsonNode は不変です。ツリーを作成・変更するには ObjectNode/ArrayNode を使用します。

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

ObjectMapper mapper = new ObjectMapper();

// 新しいオブジェクトの作成
ObjectNode obj = mapper.createObjectNode();
obj.put("id", 10);
obj.put("name", "Bob");

// 配列の追加
ArrayNode arr = mapper.createArrayNode();
arr.add("Java").add("JSON");
obj.set("tags", arr);

System.out.println(obj.toPrettyString());
/*
{
  "id" : 10,
  "name" : "Bob",
  "tags" : [ "Java", "JSON" ]
}
*/

3. Gson を使った操作: JsonElement, JsonObject, JsonArray

Jackson だけが選択肢ではありません。Gson も動的な JSON のための便利な API を提供します:JsonElementJsonObjectJsonArray

JsonElement へのパース

import com.google.gson.JsonElement;
import com.google.gson.JsonParser;

String json = "{\"id\":123,\"name\":\"Alice\",\"tags\":[\"java\",\"json\"]}";
JsonElement root = JsonParser.parseString(json);

フィールドへのアクセス

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

JsonObject obj = root.getAsJsonObject();
int id = obj.get("id").getAsInt();
String name = obj.get("name").getAsString();
JsonArray tags = obj.getAsJsonArray("tags");

for (JsonElement tag : tags) {
    System.out.println(tag.getAsString());
}

入れ子と安全性

if (obj.has("email")) {
    String email = obj.get("email").getAsString();
}

配列の取り扱い

String arrJson = "[{\"id\":1},{\"id\":2}]";
JsonArray arr = JsonParser.parseString(arrJson).getAsJsonArray();

for (JsonElement el : arr) {
    JsonObject item = el.getAsJsonObject();
    System.out.println(item.get("id").getAsInt());
}

変更

Gson のオブジェクトは可変です — フィールドの追加や削除ができます:

import com.google.gson.JsonObject;

JsonObject newObj = new JsonObject();
newObj.addProperty("id", 42);
newObj.add("tags", tags);
System.out.println(newObj.toString());

4. 実践: 構造不明な JSON からデータを取り出す

課題: 構造が変化しうる JSON 設定があるとします:

{
  "service": "mail",
  "enabled": true,
  "params": {
    "host": "smtp.example.com",
    "port": 587
  }
}

取り出すもの: hostport(Jackson を使用):

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

ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
JsonNode params = root.path("params");
String host = params.path("host").asText();
int port = params.path("port").asInt();

System.out.println(host + ":" + port); // smtp.example.com:587

Gson で同じことを行う:

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

JsonObject rootObj = JsonParser.parseString(json).getAsJsonObject();
JsonObject params = rootObj.getAsJsonObject("params");
String host = params.get("host").getAsString();
int port = params.get("port").getAsInt();

System.out.println(host + ":" + port);

5. 典型的なミスと注意点

誤り1: 型変換の誤り。 文字列を期待しているのにフィールドが数値や null の場合、asText()getAsString() は予期しない結果や例外を引き起こします。たとえば、フィールドが存在しない場合に root.get("foo").asText() を呼ぶと NullPointerException になります。

誤り2: null チェックの欠如。 特に入れ子の参照(root.get("params").get("host"))では、params が存在しないと NPE になります。Jackson では path() を使い、Gson では has で存在確認し、isJsonObject()/isJsonArray() で型を確認しましょう。

誤り3: ノード型の取り違え。 フィールドが配列なのにオブジェクトとして扱うとエラーになります。型を確認しましょう。Jackson — isArray()/isObject()、Gson — isJsonArray()/isJsonObject()

誤り4: Map/List での型情報の喪失。 Map<String, Object> にデシリアライズすると、入れ子の構造は Map/List の連なりになり、探索が複雑化して、型キャストや instanceof によるチェックが多発します。

誤り5: ツリーの(非)可変性の誤解。 Jackson の JsonNode は不変で、変更は ObjectNodeArrayNode 経由で行います。Gson のオブジェクトは可変です。ノードを変更すると、現在のツリー内のそのノードへのすべての参照に影響することを覚えておきましょう。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION