在Jackson中使用樹模型節點JsonNode

1.概述

使用JsonNode進行各種轉換以及新增,修改和刪除節點。

2.建立一個節點

建立節點的第一步是使用預設建構函式例項化ObjectMapper物件:

1
ObjectMapper mapper = new ObjectMapper();

由於建立ObjectMapper物件非常昂貴,因此建議將同一物件重複用於多個操作。

接下來,一旦有了ObjectMapper,就有三種不同的方式來建立樹節點。

2.1 從頭開始構建節點

建立節點的最常見方法如下:

1
JsonNode node = mapper.createObjectNode();

另外,也可以透過JsonNodeFactory建立一個節點:

1
JsonNode node = JsonNodeFactory.instance.objectNode();

2.2從JSON來源解析

之前文章《JSON字串轉換為JsonNode》

2.3從物件轉換

可以透過在ObjectMapper上呼叫valueToTree(Object fromValue)方法來從Java物件轉換節點:

1
JsonNode node = mapper.valueToTree(fromValue);

convertValue API在這裡也很有幫助:

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

如何使用,建立NodeBean

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NodeBean {<!-- -->
    private int id;
    private String name;
}

撰寫單元測試,確保轉化正確

1
2
3
4
5
     NodeBean fromValue = new NodeBean(2016, "123456.com");
        ObjectMapper mapper = new ObjectMapper();
        JsonNode node = mapper.valueToTree(fromValue);
        assertEquals(2016, node.get("id").intValue());
        assertEquals("123456.com", node.get("name").textValue());

3.轉換節點

3.1寫為JSON

將樹節點轉換為JSON字串的基本方法如下:

1
mapper.writeValue(destination, node);

其中目標可以是File,OutputStream或Writer。

1
2
3
4
5
6
7
public class ExampleStructure {<!-- -->
    private static ObjectMapper mapper = new ObjectMapper();

    public static JsonNode getExampleRoot() throws IOException {<!-- -->
        return mapper.createObjectNode();
    }
}
1
2
3
4
5
6
7
8
9
10
11
String newString = "{"nick": "cowtowncoder"}";
ObjectMapper mapper = new ObjectMapper();
JsonNode newNode = mapper.readTree(newString);

JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).set("name", newNode);
//{"name":{"nick":"cowtowncoder"}}
System.out.println(rootNode.toString());

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

3.2轉換為物件

將JsonNode轉換為Java物件的最方便的方法是treeToValue API:

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

在功能上等效於:

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

還可以透過令牌流來做到這一點:

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

範例:

1
2
3
4
5
6
7
8
9
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
node.put("id", 2016);
node.put("name", "baeldung.com");

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

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

4.操縱樹節點

json資料結構如下:

1
2
3
4
5
6
7
8
9
10
{<!-- -->
    "name":
        {<!-- -->
            "first": "Tatu",
            "last": "Saloranta"
        },

    "title": "Jackson founder",
    "company": "FasterXML"
}

4.1定位節點

在任何節點上工作之前,要做的第一件事是找到並將其分配給變數。

如果事先知道節點的路徑,那將很容易做到。 例如,想要一個名為last的節點,該節點位於名稱node下:

1
JsonNode locatedNode = rootNode.path("name").path("last");
1
2
3
4
5
        String json = "{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder","company":"FasterXML"}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(json);
        JsonNode locatedNode = rootNode.path("name").path("last");
        System.out.println(locatedNode); //Saloranta

另外,也可以使用get或with API代替path。

4.2新增一個新節點

可以將一個節點新增為另一個節點的子節點,如下所示:

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

put的許多過載變體可以用於新增不同實值型別的新節點。

還可以使用許多其他類似方法,包括putArray,putObject,PutPOJO,putRawValue和putNull。

最後,讓看一個範例,在該範例中,將整個結構新增到樹的根節點:

1
2
3
4
5
6
"address":
{<!-- -->
    "city": "Seattle",
    "state": "Washington",
    "country": "United States"
}
1
2
3
4
5
6
7
8
9
10
String json = "{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder","company":"FasterXML"}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(json);
        ObjectNode addedNode = ((ObjectNode) rootNode).putObject("address");
        addedNode
                .put("city", "Seattle")
                .put("state", "Washington")
                .put("country", "United States");
        System.out.println(rootNode);
        //{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder","company":"FasterXML","address":{"city":"Seattle","state":"Washington","country":"United States"}}

4.3編輯節點

可以透過呼叫set(String fieldName,JsonNode value)方法來修改ObjectNode例項:

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

透過對相同型別的物件使用replace或setAll方法,可以得到類似的結果。

1
2
3
4
5
6
7
8
9
ObjectMapper mapper = new ObjectMapper();
String newString = "{"nick": "cowtowncoder"}";
JsonNode newNode = mapper.readTree(newString);

JsonNode rootNode = mapper.createObjectNode();
((ObjectNode) rootNode).set("name", newNode);
System.out.println(rootNode);//{"name":{"nick":"cowtowncoder"}}
System.out.println(rootNode.path("name").path("nick").isMissingNode()); //false
System.out.println(rootNode.path("name").path("nick").textValue());//cowtowncoder

4.4移除一個節點

可以透過在父節點上呼叫remove(String fieldName)API來刪除該節點:

1
JsonNode removedNode = locatedNode.remove(fieldName);

為了一次刪除多個節點,可以使用Collection 型別的引數呼叫一個過載方法,該方法將回傳父節點而不是要刪除的父節點:

1
ObjectNode locatedNode = locatedNode.remove(fieldNames);

範例:

1
2
3
4
5
6
   String json = "{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder","company":"FasterXML"}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(json);
        ((ObjectNode) rootNode).remove("company");
        System.out.println(rootNode.path("company").isMissingNode());//true
        System.out.println(rootNode); //{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder"}

5.遍歷節點

遍歷JSON檔案中的所有節點,並將它們重新格式化為YAML。 JSON具有三種型別的節點,分別是**Value,Object和Array**。

資料結構如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{<!-- -->
    "name":
        {<!-- -->
            "first": "Tatu",
            "last": "Saloranta"
        }, //Object型別節點
    "title": "Jackson founder",   //Value型別節點
    "company": "FasterXML", //Value型別節點
    "pets" : [ //Array型別節點
        {<!-- -->
            "type": "dog",
            "number": 1
        },
        {<!-- -->
            "type": "fish",
            "number": 50
        }
    ]
}

生成的YAML:

1
2
3
4
5
6
7
8
9
10
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。

將JSON檔案的根節點提供給的toYaml方法,輸出轉化結果:

1
2
3
4
5
6
7
8
9
 @Test
    public void test15() throws IOException {<!-- -->
        String json = "{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder","company":"FasterXML","pets":[{"type":"dog","number":1},{"type":"fish","number":50}]}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(json);
        JsonNodeIterator jsonNodeIterator = new JsonNodeIterator();
        String s = jsonNodeIterator.toYaml(rootNode);
        System.out.println(s);
    }
1
2
3
4
5
6
//JsonNodeIterator類
public String toYaml(JsonNode root) {<!-- -->
    StringBuilder yaml = new StringBuilder();
    processNode(root, yaml, 0);
    return yaml.toString(); }
}

5.2處理不同的節點型別

需要稍微不同地處理不同型態的節點。 在processNode方法中執行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void processNode(JsonNode jsonNode, StringBuilder yaml, int depth) {<!-- -->
        //是一個value
        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。 但是,無法從元素中確定欄位名稱,因為它僅包含欄位值,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
 @Test
    public void test16() throws IOException {<!-- -->
        String json = "{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder","company":"FasterXML","pets":[{"type":"dog","number":1},{"type":"fish","number":50}]}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(json);
        Iterator<JsonNode> elements = rootNode.elements();
        while(elements.hasNext()){<!-- -->
            JsonNode next = elements.next(); //緊輸出值 不包含欄位名稱
            System.out.println(next);
        }
    }

輸出結果如下:

1
2
3
4
5
6
Object {"first": "Tatu", "last": "Saloranta"}

Value "Jackson Founder" Value "FasterXML"

Array  [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]
不包含 Obejct Value Array

相反,可以使用JsonNode.fields,因為可以訪問欄位名稱和值:

1
2
3
4
5
6
7
8
9
10
11
  @Test
    public void test17() throws IOException {<!-- -->
        String json = "{"name":{"first":"Tatu","last":"Saloranta"},"title":"Jackson founder","company":"FasterXML","pets":[{"type":"dog","number":1},{"type":"fish","number":50}]}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(json);
        Iterator<Map.Entry<String, JsonNode>> fields = rootNode.fields();
        while(fields.hasNext()){<!-- -->
            Map.Entry<String, JsonNode> next = fields.next();
            System.out.println(next);
        }
    }

輸出結果如下:

1
2
3
4
name={"first":"Tatu","last":"Saloranta"}
title="Jackson founder"
company="FasterXML"
pets=[{"type":"dog","number":1},{"type":"fish","number":50}]

對於每個欄位,將欄位名稱新增到輸出中。 然後將值傳遞給processNode方法,將其作為子節點處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 private void appendNodeToYaml(JsonNode node, StringBuilder yaml, int depth, boolean isArrayItem) {<!-- -->
        //用於訪問此JSON物件的所有欄位(包含名稱和值)的方法。
        Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
        boolean isFirst = true;
        while (fields.hasNext()) {<!-- -->
            //取得 jsonNode 名稱 和 值 key=value
            Map.Entry<String, JsonNode> jsonField = fields.next();
            //jsonField.getKey() 取得 key
            addFieldNameToYaml(yaml, jsonField.getKey(), depth, isArrayItem && isFirst);
            //jsonField.getValue() 取得value
            processNode(jsonField.getValue(), yaml, depth+1);
            isFirst = false;
        }

    }

無法從該節點知道它深度是多少。 因此,將一個稱為depth的欄位傳遞到processNode方法中,以對此進行追蹤。 每次獲得子節點時,我們都會增加此值,以便可以正確縮進YAML輸出中的欄位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void addFieldNameToYaml(
  StringBuilder yaml, String fieldName, int depth, boolean isFirstInArray) {<!-- -->
    if (yaml.length()>0) {<!-- -->
        yaml.append("
");
        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輸出,可以執行測試以表明其有效。

完成程式碼如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class JsonNodeIterator {<!-- -->

    private static final String NEW_LINE = "
";
    private static final String FIELD_DELIMITER = ": ";
    private static final String ARRAY_PREFIX = "- ";
    private static final String YAML_PREFIX = "  ";

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

    private void processNode(JsonNode jsonNode, StringBuilder yaml, int depth) {<!-- -->
        //是一個value
        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);
        }
    }

    private void appendNodeToYaml(JsonNode node, StringBuilder yaml, int depth, boolean isArrayItem) {<!-- -->
        //用於訪問此JSON物件的所有欄位(包含名稱和值)的方法。
        Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
        boolean isFirst = true;
        while (fields.hasNext()) {<!-- -->
            //取得 jsonNode 名稱 和 值 key=value
            Map.Entry<String, JsonNode> jsonField = fields.next();
            //jsonField.getKey() 取得 key
            addFieldNameToYaml(yaml, jsonField.getKey(), depth, isArrayItem && isFirst);
            //jsonField.getValue() 取得value
            processNode(jsonField.getValue(), yaml, depth+1);
            isFirst = false;
        }

    }

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

}