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 對象的 valueToTree(Object fromValue) 方法,可以將節點從 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. 節點轉換
3.1. 寫入為 JSON
這是將樹節點轉換為 JSON 字符串的基本方法,目標可以是 File、OutputStream 或 Writer:
mapper.writeValue(destination, node);
通過重用類 NodeBean,聲明在第 2.3 節,一個測試確保此方法按預期工作:
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. 轉換為對象
將 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. Manipulating Tree Nodes
我們將使用以下 JSON 元素,存儲在名為 example.json 的文件中,作為操作的基礎結構:
{
"name": {
"first": "Tatu",
"last": "Saloranta"
},
"title": "Jackson founder",
"company": "FasterXML"
}
此 JSON 文件位於類路徑上,已解析為模型樹:
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. Locating a Node
在對任何節點進行操作之前,第一件事是找到並將其分配給一個變量。
如果事先知道節點的路徑,那很容易做到。
例如,我們想要一個名為 last 的節點,該節點位於 name 節點下:
JsonNode locatedNode = rootNode.path("name").path("last");
或者,也可以使用 get 或 with API 而不是 path。
如果路徑未知,則搜索將變得更加複雜和迭代。
我們可以查看在第 5 節 – 遍歷節點中的示例,該節稱為 Iterating Over the Nodes。
4.2. Adding a New Node
可以通過將節點作為另一個節點的子節點來添加節點:
ObjectNode newNode = ((ObjectNode) locatedNode).put(fieldName, value);
可以使用多種重載的 put 方法來添加不同類型的值的新節點。
還有許多其他類似的方法,包括 putArray、putObject、PutPOJO、putRawValue 和 putNull。
最後,讓我們看一下添加整個結構到樹的根節點的一個例子:
"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. Editing a Node
可以使用 set(String fieldName, JsonNode value) 方法修改 ObjectNode 實例:
JsonNode locatedNode = locatedNode.set(fieldName, value);
可以使用 replace 或 setAll 方法在相同類型的對象上實現類似結果。
為了驗證該方法按預期工作,我們將更改根節點下 name 字段的值,從一個由 first 和 last 字段組成的對象到另一個只包含 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. Removing a Node
可以通過在父節點上調用 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. Iterating Over the Nodes
Let’s iterate over all the nodes in a JSON document and reformat them into YAML.
JSON has three types of nodes, which are Value, Object and Array.
So, let’s ensure our sample data has all three different types by adding an Array:
{
"name":
{
"first": "Tatu",
"last": "Saloranta"
},
"title": "Jackson founder",
"company": "FasterXML",
"pets" : [
{
"type": "dog",
"number": 1
},
{
"type": "fish",
"number": 50
}
]
}
Now let’s see the YAML we want to produce:
name:
first: Tatu
last: Saloranta
title: Jackson founder
company: FasterXML
pets:
- type: dog
number: 1
- type: fish
number: 50
We know that JSON nodes have a hierarchical tree structure. So, the easiest way to iterate over the whole JSON document is to start at the top and work our way down through all the child nodes.
We’ll pass the root node into a recursive method. The method will then call itself with each child of the supplied node.
5.1. Testing the Iteration
We’ll start by creating a simple test that checks that we can successfully convert the JSON to YAML.
Our test supplies the root node of the JSON document to our toYaml method and asserts the returned value is what we expect:
@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. Handling Different Node Types
We need to handle different types of nodes slightly differently.
We’ll do this in our processNode method:
private void processNode(JsonNode jsonNode, StringBuilder yaml, int depth) {
if (jsonNode.isValueNode()) {
yaml.append(jsonNode.asText());
}
else if (jsonNode.isArrays()) {
for (JsonNode arrayItem : jsonNode) {
appendNodeToYaml(arrayItem, yaml, depth, true);
}
}
else if (jsonNode.isObject()) {
appendNodeToYaml(jsonNode, yaml, depth, false);
}
}
First, let’s consider a Value node. We simply call the asText method of the node to get a String representation of the value.
Next, let’s look at an Array node. Each item within the Array node is itself a JsonNode, so we iterate over the Array and pass each node to the appendNodeToYaml method. We also need to know that these nodes are part of an array.
Unfortunately, the node itself does not contain anything that tells us that, so we’ll pass a flag into our appendNodeToYaml method.
Finally, we want to iterate over all the child nodes of each Object node. One option is to use JsonNode.elements.
However, we can’t determine the field name from an element because it just contains the field value:
Object {"first": "Tatu", "last": "Saloranta"}
Value "Jackson Founder"
Value "FasterXML"
Array [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]
Instead, we’ll use JsonNode.fields because this gives us access to both the field name and value:
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}]
For each field, we add the field name to the output and then process the value as a child node by passing it to the processNode method:
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;
}
}
We can’t tell from the node how many ancestors it has.
So, we pass a field called depth into the processNode method to keep track of this, and we increment this value each time we get a child node so that we can correctly indent the fields in our YAML output:
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(": ");
}
Now that we have all the code in place to iterate over the nodes and create the YAML output, we can run our test to show that it works.
6. 結論
本文介紹了在 Jackson 中使用樹模型時常用的 API 和場景。