1. 概述

本文将深入探讨如何在 Jackson 中操作 树模型节点(Tree Model Nodes)

我们将使用 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. 从对象转换而来

通过调用 ObjectMappervalueToTree(Object fromValue) 方法,可以将一个 Java 对象转换为 JsonNode

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;
    }

    // 标准的 getter 和 setter 省略
}

下面的测试验证转换是否正确:

@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 字符串的基本方法,目标可以是 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. 转换为 Java 对象

最方便的方式是使用 treeToValue API:

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

这与下面的写法功能一致:

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

还可以通过 token 流的方式:

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 内容作为示例结构(位于 classpath 的 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. 定位节点

在操作节点前,首先要定位到它:

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

也可以使用 getwith 代替 path

如果路径未知,则需要通过迭代方式查找(见 第 5 节)。

4.2. 添加新节点

可以通过以下方式向节点添加子节点:

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. 修改节点

通过 set(String fieldName, JsonNode value) 修改节点:

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

也可使用 replacesetAll 方法。

测试:将 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) 删除节点:

JsonNode removedNode = locatedNode.remove(fieldName);

要删除多个字段,可使用 remove(Collection<String>)

ObjectNode locatedNode = locatedNode.remove(fieldNames);

如果要清空所有子节点,可使用 removeAll()

测试删除 company 字段:

@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 有三种节点类型:Value、Object 和 Array。

我们通过添加一个 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

5.1. 测试遍历

测试代码:

@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. 处理不同节点类型

处理逻辑如下:

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() 获取值。
  • Array 节点:遍历数组,传入 appendNodeToYaml
  • Object 节点:使用 fields() 获取字段名和值。
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;
    }
}
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(": ");
}

6. 总结

本文涵盖了 Jackson 树模型中的常用 API 和操作场景。

所有示例代码均可在 GitHub 仓库中找到:**GitHub 项目地址**。


原始标题:Working with Tree Model Nodes in Jackson