Jackson 是 Java 裡最強的 JSON 庫,但新手常搞不清楚 JsonNode 的各種遍歷方法,還有 path() 和 get() 的差別。踩過坑以後,整理出來給大家。

JsonNode 遍歷

拿到一個 JsonNode,要怎麼遍歷裡面的欄位?有好幾種方法。

方法 1:用 fields() 取得 Iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ObjectMapper mapper = new ObjectMapper();
String json = """
{
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
""";

JsonNode node = mapper.readTree(json);

// fields() 回傳 Iterator<Map.Entry<String, JsonNode>>
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String key = entry.getKey();
JsonNode value = entry.getValue();

System.out.println(key + " = " + value.asText());
}

// 輸出:
// name = Alice
// age = 30
// email = alice@example.com

方法 2:用 fieldNames() 只取 key

如果只關心 key:

1
2
3
4
5
6
7
8
9
10
Iterator<String> fieldNames = node.fieldNames();
while (fieldNames.hasNext()) {
String key = fieldNames.next();
System.out.println(key);
}

// 輸出:
// name
// age
// email

方法 3:forEach 遍歷(Java 8+)

更現代的做法,用 forEach:

1
2
3
4
5
node.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
System.out.println(key + " = " + value.asText());
});

或者直接用 forEach 方法:

1
2
3
node.forEach(child -> {
System.out.println(child);
});

這個 forEach 直接遍歷值(JsonNode),不包含 key。

方法 4:用 toPrettyString() 看結構

開發時一堆 debug,直接印出來看:

1
System.out.println(node.toPrettyString());

實戰:Template 變數替換

最常見的應用場景:有個模板文字,要用 JSON 的 key-value 替換裡面的變數。

比如模板:

1
Hello {{name}}, you are {{age}} years old. Contact: {{email}}

用 JsonNode 去替換:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
String template = "Hello {{name}}, you are {{age}} years old. Contact: {{email}}";
String json = """
{
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
""";

ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(json);

String result = template;

// 遍歷 JSON 的所有欄位
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String key = entry.getKey();
String value = entry.getValue().asText();

// 替換 {{key}} 為實際值
result = result.replace("{{" + key + "}}", value);
}

System.out.println(result);
// 輸出:Hello Alice, you are 30 years old. Contact: alice@example.com

或者用 regex 更優雅(但對複雜模板可能有問題):

1
2
3
4
5
6
String result = template;
node.fields().forEachRemaining(entry -> {
String key = entry.getKey();
String value = entry.getValue().asText();
result = result.replace("{{" + key + "}}", value);
});

path() vs get() 的差別

這是最容易踩的坑。

get() - 屬性不存在回傳 null

1
2
3
4
5
6
7
8
9
10
JsonNode user = mapper.readTree("""
{
"name": "Alice",
"age": 30
}
""");

JsonNode email = user.get("email"); // 屬性不存在
System.out.println(email); // null
System.out.println(email.asText()); // NullPointerException !!!

path() - 屬性不存在回傳 MissingNode

1
2
3
4
5
6
7
8
9
10
11
JsonNode user = mapper.readTree("""
{
"name": "Alice",
"age": 30
}
""");

JsonNode email = user.path("email"); // 屬性不存在
System.out.println(email); // (empty)
System.out.println(email.asText()); // "" (空字串,不會爆)
System.out.println(email.isMissingNode()); // true

實戰例子

所以安全的做法是永遠用 path(),然後檢查 isMissingNode():

1
2
3
4
5
6
7
8
9
10
11
12
public String extractEmail(String json) throws Exception {
JsonNode node = mapper.readTree(json);

// 用 path() 避免 NPE
JsonNode email = node.path("email");

if (email.isMissingNode()) {
return "No email provided";
}

return email.asText();
}

或者簡單做法,path() 直接 asText() 就會回傳空字串:

1
String email = node.path("email").asText();  // 不存在就是 "",不會爆

複雜路徑

要取嵌套的欄位,path() 特別方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
String json = """
{
"user": {
"profile": {
"email": "alice@example.com"
}
}
}
""";

JsonNode node = mapper.readTree(json);

// 用 path 直接取深層欄位
String email = node.path("user")
.path("profile")
.path("email")
.asText();

System.out.println(email); // alice@example.com

// 如果任何中間欄位不存在,結果還是空字串,不會爆
String phone = node.path("user")
.path("contact")
.path("phone")
.asText();

System.out.println(phone); // "" (contact 不存在,但沒爆)

常用的轉換方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
JsonNode node = mapper.readTree(json);

// 轉字串
String str = node.asText(); // 預設空字串

// 轉 int
int num = node.asInt(); // 預設 0

// 轉 long
long lng = node.asLong(); // 預設 0L

// 轉 double
double dbl = node.asDouble(); // 預設 0.0

// 轉 boolean
boolean bool = node.asBoolean(); // 預設 false

// 帶預設值
String str = node.asText("default value");
int num = node.asInt(999);

檢查型態

有時候要確認欄位是啥型態:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (node.isObject()) {
// 物件型
}

if (node.isArray()) {
// 陣列型
}

if (node.isTextual()) {
// 字串型
}

if (node.isNumber()) {
// 數字型
}

if (node.isBoolean()) {
// 布林型
}

if (node.isNull()) {
// null
}

if (node.isMissingNode()) {
// 不存在的欄位(用 path() 才會回傳)
}

遍歷陣列

JSON 陣列怎麼辦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
String json = """
{
"users": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Cathy", "age": 28}
]
}
""";

JsonNode root = mapper.readTree(json);
JsonNode users = root.path("users");

// 方法 1:for 迴圈
for (JsonNode user : users) {
String name = user.path("name").asText();
int age = user.path("age").asInt();
System.out.println(name + ": " + age);
}

// 方法 2:forEach
users.forEach(user -> {
String name = user.path("name").asText();
int age = user.path("age").asInt();
System.out.println(name + ": " + age);
});

// 輸出:
// Alice: 30
// Bob: 25
// Cathy: 28

實務建議

  1. 永遠用 path() 而不是 get()

    • path() 安全,不存在就回傳 MissingNode,asText() 回傳空字串
    • get() 回傳 null,容易踩 NullPointerException
  2. 遍歷物件用 fields()

    1
    2
    3
    4
    node.fields().forEachRemaining(entry -> {
    String key = entry.getKey();
    JsonNode value = entry.getValue();
    });
  3. 遍歷陣列直接 for-each 或 forEach

    1
    2
    3
    for (JsonNode item : node) {
    // 處理 item
    }
  4. 複雜嵌套路徑用 path() 鏈式呼叫

    1
    node.path("a").path("b").path("c").asText()
  5. 要檢查欄位是否存在就用 isMissingNode()

    1
    2
    3
    if (!node.path("optional_field").isMissingNode()) {
    // 欄位存在才處理
    }

Jackson 其實沒那麼難,關鍵是理解 path() 和 get() 的差別,然後就沒有踩不完的坑了。