# JsonNode 在 Spring 中的使用筆記
`JsonNode` 是 Jackson 函式庫(Spring Boot 內建)用來表示「任意 JSON 結構」的物件模型,
不需要事先定義 POJO 就能讀寫 JSON。適合處理欄位不固定、動態、或第三方回傳結構多變的情境。
---
## 1. 基本概念
- `JsonNode`:唯讀的樹狀節點介面(抽象基底)
- `ObjectNode`:對應 JSON object `{}`,可讀可寫
- `ArrayNode`:對應 JSON array `[]`,可讀可寫
- `ObjectMapper`:Jackson 的核心工具,負責解析 / 產生 JsonNode
```java
@Autowired
private ObjectMapper objectMapper; // Spring Boot 已自動註冊為 Bean,不需自己 new
```
> Spring Boot 專案只要有 `spring-boot-starter-web`,Jackson 就已經在 classpath 裡,
> `ObjectMapper` 也已經是可注入的 Bean(含自動設定的日期格式等)。
---
## 2. 讀取 JSON 字串 / 轉出 JsonNode
```java
String json = "{\"name\":\"Adam\",\"age\":30,\"tags\":[\"java\",\"spring\"]}";
JsonNode root = objectMapper.readTree(json);
String name = root.get("name").asText(); // "Adam"
int age = root.get("age").asInt(); // 30
JsonNode tagsNode = root.get("tags"); // ArrayNode
```
也可以直接從 `InputStream`、`File`、`byte[]` 讀:
```java
JsonNode node = objectMapper.readTree(inputStream);
```
---
## 3. `get()` vs `path()`:最容易踩的坑
| 方法 | 欄位不存在時 | 用途建議 |
|------|-------------|---------|
| `get("key")` | 回傳 `null`(Java null) | 你確定欄位存在時使用 |
| `path("key")` | 回傳 `MissingNode`(不是 null,是特殊 JsonNode) | 欄位可能不存在,避免 NPE 時使用 |
```java
root.get("notExist"); // null -> 直接 .asText() 會 NPE
root.path("notExist"); // MissingNode -> .asText() 回傳 "" 安全
root.path("notExist").isMissingNode(); // true
```
**建議:欄位不確定是否存在時一律用 `path()`,並搭配 `isMissingNode()` / `isNull()` 判斷。**
---
## 4. 常用判斷 / 取值方法
```java
node.isNull(); // JSON 的 null 值(欄位存在但值是 null)
node.isMissingNode(); // 欄位完全不存在(只有 path() 會回傳這個)
node.isObject();
node.isArray();
node.isTextual();
node.isNumber();
node.isBoolean();
node.asText(); // 轉字串(會自動 toString 其他型別)
node.asText("預設值"); // 帶預設值版本
node.asInt();
node.asInt(0);
node.asLong();
node.asDouble();
node.asBoolean();
```
> `asXxx()` 系列方法對型別不符或節點缺失時通常回傳「安全預設值」而不是丟例外,
> 這點跟直接 cast 或用 `.textValue()` 不同,要注意混用時的行為差異。
---
## 5. 走訪 ObjectNode / ArrayNode
```java
// 走訪 object 的所有欄位
Iterator<Map.Entry<String, JsonNode>> fields = root.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// 走訪 array
for (JsonNode tag : root.path("tags")) {
System.out.println(tag.asText());
}
// 巢狀路徑一次取值(Jackson 2.10+)
String city = root.at("/address/city").asText();
```
---
## 6. 建立 JSON(寫入)
```java
ObjectNode obj = objectMapper.createObjectNode();
obj.put("name", "Adam");
obj.put("age", 30);
obj.put("active", true);
ArrayNode tags = obj.putArray("tags");
tags.add("java");
tags.add("spring");
ObjectNode address = obj.putObject("address");
address.put("city", "Taipei");
String json = objectMapper.writeValueAsString(obj);
```
輸出:
```json
{"name":"Adam","age":30,"active":true,"tags":["java","spring"],"address":{"city":"Taipei"}}
```
---
## 7. JsonNode 與 POJO 互轉
```java
// JsonNode -> POJO
MyDto dto = objectMapper.treeToValue(jsonNode, MyDto.class);
// POJO -> JsonNode
JsonNode node = objectMapper.valueToTree(dto);
// Map -> JsonNode
JsonNode node2 = objectMapper.valueToTree(someMap);
```
適合「部分欄位固定、部分欄位動態」的情境:先轉成 JsonNode 抓動態部分,
再把已知結構的部分轉 POJO。
---
## 8. 在 Spring MVC / Controller 中使用
### 8.1 接收任意 JSON body
```java
@PostMapping("/webhook")
public ResponseEntity<Void> handleWebhook(@RequestBody JsonNode payload) {
String eventType = payload.path("type").asText();
if ("payment.success".equals(eventType)) {
JsonNode data = payload.path("data");
// ...
}
return ResponseEntity.ok().build();
}
```
適合第三方 webhook(金流、通知服務)這種欄位可能隨版本增減、
但你只關心其中幾個欄位的情境 —— 不用為了每個 webhook 事件類型都建一個 DTO。
### 8.2 回傳動態結構
```java
@GetMapping("/dynamic")
public JsonNode dynamicResponse() {
ObjectNode result = objectMapper.createObjectNode();
result.put("status", "ok");
result.putArray("items").add("a").add("b");
return result;
}
```
Spring 會透過 Jackson `HttpMessageConverter` 自動序列化 `JsonNode`,
效果等同回傳一般 DTO。
### 8.3 搭配 RestTemplate / WebClient 呼叫外部 API 且回應結構不穩定
```java
JsonNode response = restTemplate.getForObject(url, JsonNode.class);
String value = response.path("result").path("value").asText();
```
不需要為每個上游 API 都建對應的 Response DTO,尤其是欄位常變動或
只需要抓其中一兩個值的情境。
---
## 9. 常見使用情境(何時該用 JsonNode 而非 POJO)
- Webhook / callback 接收端:payload 結構隨事件類型不同
- 呼叫外部 / 第三方 API,回應結構不穩定或只需要部分欄位
- 需要「原封不動轉存」JSON(例如寫入 log、存 DB 的 JSONB 欄位)
- 動態 schema、多版本相容(新舊欄位並存)
- 寫通用的 JSON 比對 / 轉換工具,不知道會吃到什麼結構
**不建議**用在:結構固定、有明確 API 合約的情境 —— 那種情況用 POJO/DTO
可以拿到編譯期型別檢查與更好的可讀性,不要為了「彈性」放棄型別安全。
---
## 10. 小提醒
- `JsonNode` 本身是唯讀的,`ObjectNode` / `ArrayNode` 才可變
- 序列化時 `JsonNode` 會照原樣輸出,不會套用 `@JsonProperty` 等 POJO 上的註解
- 大量資料時 `readTree()` 會整棵樹讀進記憶體,資料量非常大時考慮 Streaming API(`JsonParser`)
- 判斷「欄位不存在」與「欄位值為 null」是兩件事,混用 `get`/`path` 容易踩雷(見第 3 節)