知識庫 / JSON / Jackson RSS 訂閱

使用Jackson處理樹模型節點

Data,Jackson
HongKong
10
09:57 PM · Dec 05 ,2025

1. 概述

本教程將重點介紹使用 Jackson 中的樹模型節點

我們將使用 JsonNode 進行各種轉換,以及添加、修改和刪除節點。

2. 創建節點

創建節點的第一步是使用默認構造函數實例化一個 ObjectMapper 對象:

ObjectMapper mapper = new ObjectMapper();

由於創建 ObjectMapper 對象本身成本較高,因此建議我們為多項操作重用同一個對象。

接下來,當我們擁有 ObjectMapper 對象後,有三種不同的方法可以創建樹節點。

2.1. 從零創建節點

這是創建節點最常見的方法:

JsonNode node = mapper.createObjectNode();

當然,以下是翻譯後的內容:

或者,我們還可以通過 JsonNodeFactory 創建節點:

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. 從 JSON 來源解析

此方法在 Jackson – Marshall String to JsonNode 文章中得到了充分的闡述。請參閲該文章以獲取更多信息。

2.3. 從對象轉換

一個節點可以通過調用 ObjectMapper 對象的 <em>valueToTree(Object fromValue)</em> 方法,從 Java 對象中轉換:

JsonNode node = mapper.valueToTree(fromValue);

convertValue API 在這裏也很有用:

JsonNode node = mapper.convertValue(fromValue, JsonNode.class);

讓我們看看實際應用效果。

假設我們有一個名為 NodeBean 的類:

public class NodeBean {
    private int id;
    private String name;

    public NodeBean() {
    }

    public NodeBean(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // standard getters and setters
}

讓我們編寫一個測試,以確保轉換正確進行:

@Test
public void givenAnObject_whenConvertingIntoNode_thenCorrect() {
    NodeBean fromValue = new NodeBean(2016, "baeldung.com");

    JsonNode node = mapper.valueToTree(fromValue);

    assertEquals(2016, node.get("id").intValue());
    assertEquals("baeldung.com", node.get("name").textValue());
}

3. 轉換一個 Node

/**
 * This function transforms a Node object into a new Node object.
 * It modifies the original Node object.
 *
 * @param {Node} node The Node object to transform.
 * @returns {Node} The transformed Node object.
 */
function transformNode(node) {
  // Perform the transformation logic here.
  // This could involve updating properties,
  // adding new attributes, or removing existing ones.

  // Example:
  // node.name = "New Name";
  // node.value = node.value * 2;

  return node;
}

3.1. 以 JSON 格式輸出

這是將樹節點轉換為 JSON 字符串的基本方法,目標可以是 FileOutputStreamWriter

mapper.writeValue(destination, node);

通過重用在第 2.3 節中聲明的 NodeBean 類,一個測試確保該方法按預期工作:

final String pathToTestFile = "node_to_json_test.json";

@Test
public void givenANode_whenModifyingIt_thenCorrect() throws IOException {
    String newString = "{\"nick\": \"cowtowncoder\"}";
    JsonNode newNode = mapper.readTree(newString);

    JsonNode rootNode = ExampleStructure.getExampleRoot();
    ((ObjectNode) rootNode).set("name", newNode);

    assertFalse(rootNode.path("name").path("nick").isMissingNode());
    assertEquals("cowtowncoder", rootNode.path("name").path("nick").textValue());
}

3.2. 將 JSON 節點轉換為對象

最方便的方法是將 JsonNode 對象轉換為 Java 對象,就是使用 treeToValue API:

NodeBean toValue = mapper.treeToValue(node, NodeBean.class);

這與以下內容功能上等效:

NodeBean toValue = mapper.convertValue(node, NodeBean.class)

我們也可以通過令牌流來實現:

JsonParser parser = mapper.treeAsTokens(node);
NodeBean toValue = mapper.readValue(parser, NodeBean.class);

最後,讓我們實現一個測試,以驗證轉換過程:

@Test
public void givenANode_whenConvertingIntoAnObject_thenCorrect()
  throws JsonProcessingException {
    JsonNode node = mapper.createObjectNode();
    ((ObjectNode) node).put("id", 2016);
    ((ObjectNode) node).put("name", "baeldung.com");

    NodeBean toValue = mapper.treeToValue(node, NodeBean.class);

    assertEquals(2016, toValue.getId());
    assertEquals("baeldung.com", toValue.getName());
}

4. 操縱樹節點

我們將使用以下 JSON 元素,存儲在名為 example.json 的文件中,作為執行操作的基礎結構:

{
    "name": 
        {
            "first": "Tatu",
            "last": "Saloranta"
        },

    "title": "Jackson founder",
    "company": "FasterXML"
}
<p>此JSON文件位於類路徑上,被解析為一個模型樹。</p>
public class ExampleStructure {
    private static ObjectMapper mapper = new ObjectMapper();

    static JsonNode getExampleRoot() throws IOException {
        InputStream exampleInput = 
          ExampleStructure.class.getClassLoader()
          .getResourceAsStream("example.json");
        
        JsonNode rootNode = mapper.readTree(exampleInput);
        return rootNode;
    }
}

請注意,樹的根將用於説明後續子章節中節點的操作。

4.1. 查找節點

在對任何節點進行操作之前,第一件事就是要找到並將其分配給一個變量。

如果事先知道節點的路徑,那很容易做到。

例如,我們想要一個名為 last 的節點,該節點位於 name 節點下:

JsonNode locatedNode = rootNode.path("name").path("last");

或者,getwith API 也可以代替 path

如果不知道路徑,搜索將自然變得更加複雜和迭代。

可以在 第 5 節 – 遍歷節點 中看到一個遍歷所有節點的示例。

4.2. 添加新節點

一個節點可以作為另一個節點的子節點添加:

ObjectNode newNode = ((ObjectNode) locatedNode).put(fieldName, value);

許多重載的 put 變體可用於添加不同類型的值節點。

還有許多其他類似的的方法可用,包括 putArrayputObjectPutPOJOputRawValueputNull

最後,讓我們來看一個示例,其中我們向樹的根節點添加整個結構:

"address":
{
    "city": "Seattle",
    "state": "Washington",
    "country": "United States"
}

以下是所有操作的完整測試,並驗證了結果:

@Test
public void givenANode_whenAddingIntoATree_thenCorrect() throws IOException {
    JsonNode rootNode = ExampleStructure.getExampleRoot();
    ObjectNode addedNode = ((ObjectNode) rootNode).putObject("address");
    addedNode
      .put("city", "Seattle")
      .put("state", "Washington")
      .put("country", "United States");

    assertFalse(rootNode.path("address").isMissingNode());
    
    assertEquals("Seattle", rootNode.path("address").path("city").textValue());
    assertEquals("Washington", rootNode.path("address").path("state").textValue());
    assertEquals(
      "United States", rootNode.path("address").path("country").textValue();
}

4.3. 修改節點

可以通過調用 set(String fieldName, JsonNode value) 方法來修改一個 ObjectNode 實例:

JsonNode locatedNode = locatedNode.set(fieldName, value);

類似的結果可以通過在相同類型的對象上使用 replacesetAll 方法來實現。

為了驗證該方法是否按預期工作,我們將從 firstlast 對象更改根節點下的 name 字段的值,使其成為僅包含 nick 字段的一個對象,進行測試。

@Test
public void givenANode_whenModifyingIt_thenCorrect() throws IOException {
    String newString = "{\"nick\": \"cowtowncoder\"}";
    JsonNode newNode = mapper.readTree(newString);

    JsonNode rootNode = ExampleStructure.getExampleRoot();
    ((ObjectNode) rootNode).set("name", newNode);

    assertFalse(rootNode.path("name").path("nick").isMissingNode());
    assertEquals("cowtowncoder", rootNode.path("name").path("nick").textValue());
}

4.4. 刪除節點

可以通過調用其父節點的 remove(String fieldName) API 來刪除節點:

JsonNode removedNode = locatedNode.remove(fieldName);

為了一次性刪除多個節點,我們可以調用具有 Collection<String> 參數類型的重載方法,該方法返回父節點而不是要刪除的節點:

ObjectNode locatedNode = locatedNode.remove(fieldNames);

在極端情況下,當我們想要刪除給定節點的全部子節點時,removeAll API 就派上用場。

以下測試將重點關注上述提到的第一種方法,這通常是最常見的場景:

@Test
public void givenANode_whenRemovingFromATree_thenCorrect() throws IOException {
    JsonNode rootNode = ExampleStructure.getExampleRoot();
    ((ObjectNode) rootNode).remove("company");

    assertTrue(rootNode.path("company").isMissingNode());
}

5. 遍歷節點

讓我們遍歷 JSON 文檔中的所有節點,並將它們重新格式化為 YAML。

JSON 有三種類型的節點:值、對象和數組。

因此,讓我們確保樣例文檔包含這三種不同類型,通過添加一個Array來實現。

{
    "name": 
        {
            "first": "Tatu",
            "last": "Saloranta"
        },

    "title": "Jackson founder",
    "company": "FasterXML",
    "pets" : [
        {
            "type": "dog",
            "number": 1
        },
        {
            "type": "fish",
            "number": 50
        }
    ]
}

現在讓我們來看一下我們想要生成的YAML:

name: 
  first: Tatu
  last: Saloranta
title: Jackson founder
company: FasterXML
pets: 
- type: dog
  number: 1
- type: fish
  number: 50

我們知道 JSON 節點具有層級樹狀結構。因此,遍歷整個 JSON 文檔最簡單的方法是從根節點開始,然後逐層向下遍歷所有子節點。

我們將根節點傳遞給一個遞歸方法。該方法將自身調用,並傳入每個根節點的子節點。

5.1. 測試迭代

我們將首先創建一個簡單的測試,以驗證我們是否能夠成功地將 JSON 轉換為 YAML。

該測試將向我們的 toYaml 方法提供 JSON 文檔的根節點,並斷言返回的值是我們期望的值:

@Test
public void givenANodeTree_whenIteratingSubNodes_thenWeFindExpected() throws IOException {
    JsonNode rootNode = ExampleStructure.getExampleRoot();
    
    String yaml = onTest.toYaml(rootNode);

    assertEquals(expectedYaml, yaml); 
}

public String toYaml(JsonNode root) {
    StringBuilder yaml = new StringBuilder(); 
    processNode(root, yaml, 0); 
    return yaml.toString(); }
}

5.2. 處理不同節點類型

我們需要以不同的方式處理不同類型的節點。

我們將通過在我們的 processNode 方法中實現來實現:

private void processNode(JsonNode jsonNode, StringBuilder yaml, int depth) {
    if (jsonNode.isValueNode()) {
        yaml.append(jsonNode.asText());
    }
    else if (jsonNode.isArray()) {
        for (JsonNode arrayItem : jsonNode) {
            appendNodeToYaml(arrayItem, yaml, depth, true);
        }
    }
    else if (jsonNode.isObject()) {
        appendNodeToYaml(jsonNode, yaml, depth, false);
    }
}

首先,讓我們考慮一個 Value 節點。我們只需調用節點的 asText 方法,即可獲得該值的 String 表示形式。

接下來,讓我們看看 Array 節點。Array 節點中的每個項目本身就是一個 JsonNode,因此我們遍歷 Array 並將每個節點傳遞給 appendNodeToYaml 方法。我們還需要知道這些節點是數組的一部分。

不幸的是,節點本身不包含任何信息告訴我們這一點,因此我們將一個標誌傳遞給我們的 appendNodeToYaml 方法。

最後,我們想要遍歷每個 Object 節點的子節點。一個選項是使用 JsonNode.elements

但是,由於元素只包含字段值,因此我們無法從元素中確定字段名稱。

Object  {"first": "Tatu", "last": "Saloranta"}
Value  "Jackson Founder"
Value  "FasterXML"
Array  [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]

我們將會使用 JsonNode.fields,因為這能讓我們訪問字段名稱和值。

Key="name", Value=Object  {"first": "Tatu", "last": "Saloranta"}
Key="title", Value=Value  "Jackson Founder"
Key="company", Value=Value  "FasterXML"
Key="pets", Value=Array  [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]

對於每個字段,我們都會將字段名稱添加到輸出中,然後將值作為子節點處理,並通過將其傳遞給 processNode 方法:

private void appendNodeToYaml(
  JsonNode node, StringBuilder yaml, int depth, boolean isArrayItem) {
    Iterator<Entry<String, JsonNode>> fields = node.fields();
    boolean isFirst = true;
    while (fields.hasNext()) {
        Entry<String, JsonNode> jsonField = fields.next();
        addFieldNameToYaml(yaml, jsonField.getKey(), depth, isArrayItem && isFirst);
        processNode(jsonField.getValue(), yaml, depth+1);
        isFirst = false;
    }
        
}

我們無法通過節點來確定其祖先節點數量。

因此,我們向 processNode 方法傳遞一個名為 depth 的字段,用於跟蹤此信息,並在每次獲取子節點時增加該值,以便正確地縮進我們的 YAML 輸出中的字段:

private void addFieldNameToYaml(
  StringBuilder yaml, String fieldName, int depth, boolean isFirstInArray) {
    if (yaml.length()>0) {
        yaml.append("\n");
        int requiredDepth = (isFirstInArray) ? depth-1 : depth;
        for(int i = 0; i < requiredDepth; i++) {
            yaml.append("  ");
        }
        if (isFirstInArray) {
            yaml.append("- ");
        }
    }
    yaml.append(fieldName);
    yaml.append(": ");
}

現在我們已經將所有代碼部署到位,可以迭代遍歷節點並生成 YAML 輸出,我們可以運行測試以驗證其功能。

6. 結論

本文介紹了在 Jackson 中使用樹模型時常用的 API 以及相關的場景。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.