Jacksonでツリーモデルノードを操作する

ジャクソンのツリーモデルノードの操作

1. 概要

このチュートリアルでは、tree model nodes in Jacksonの操作に焦点を当てます。

さまざまな変換、およびノー​​ドの追加、変更、削除にJsonNodeを使用します。

2. ノードの作成

ノード作成の最初のステップは、デフォルトのコンストラクターを使用してObjectMapperオブジェクトをインスタンス化することです。

ObjectMapper mapper = new ObjectMapper();

ObjectMapperオブジェクトの作成にはコストがかかるため、同じオブジェクトを複数の操作に再利用することをお勧めします。

次に、ObjectMapperを取得したら、ツリーノードを作成する3つの異なる方法があります。

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 node = mapper.valueToTree(fromValue);

convertValueAPIもここで役立ちます。

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, "example.com");

    JsonNode node = mapper.valueToTree(fromValue);

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

3. ノードの変換

3.1. JSONとして書き出す

ツリーノードをJSON文字列に変換する基本的な方法は次のとおりです。

mapper.writeValue(destination, node);

ここで、宛先はFileOutputStream、またはWriterです。 セクション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. オブジェクトに変換する

JsonNodeをJavaオブジェクトに変換する最も便利な方法は、treeToValueAPIです。

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", "example.com");

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

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

4. ツリーノードの操作

example.jsonという名前のファイルに含まれる次の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. ノードの検索

ノードで作業する前に、最初に行う必要があるのは、ノードを見つけて変数に割り当てることです。

ノードへのパスが事前にわかっている場合、それは非常に簡単です。 たとえば、nameノードの下にあるlastという名前のノードが必要だとします。

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

または、pathの代わりにgetまたはwithAPIを使用することもできます。

パスがわからない場合は、もちろん、検索はより複雑で反復的になります。

5. Iterating Over the Nodesですべてのノードを反復する例を見ることができます

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. ノードの編集

ObjectNodeインスタンスは、set(String fieldName, JsonNode value)メソッドを呼び出すことで変更できます。

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

同じタイプのオブジェクトでreplaceまたはsetAllメソッドを使用しても、同様の結果が得られる可能性があります。

メソッドが期待どおりに機能することを確認するために、ルートノードの下のフィールドnameの値をfirstlastのオブジェクトから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);

極端な場合、特定のノードのすべてのサブノードを削除する場合は、removeAllAPIが役立ちます。

次のテストでは、上記の最初の方法に焦点を当てます。これは最も一般的なシナリオです。

@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の3つのタイプのノードがあります。

したがって、Array:を追加して、サンプルデータに3つの異なるタイプすべてがあることを確認しましょう。

{
    "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に正常に変換できることを確認する簡単なテストを作成します。

このテストでは、JSONドキュメントのルートノードをtoYamlメソッドに提供し、戻り値が期待どおりであることを表明します。

@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表現を取得できます。

次に、配列ノードを見てみましょう。 配列ノード内の各項目はそれ自体がJsonNodeであるため、配列を反復処理し、各ノードをappendNodeToYamlメソッドに渡します。 また、これらのノードが配列の一部であることを知る必要があります。

残念ながら、ノード自体にはそのことを示すものが含まれていないため、appendNodeToYamlメソッドにフラグを渡します。

最後に、各Objectノードのすべての子ノードを反復処理します。 1つのオプションは、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> fields = node.fields();
    boolean isFirst = true;
    while (fields.hasNext()) {
        Entry jsonField = fields.next();
        addFieldNameToYaml(yaml, jsonField.getKey(), depth, isArrayItem && isFirst);
        processNode(jsonField.getValue(), yaml, depth+1);
        isFirst = false;
    }

}

ノードからは、祖先がいくつあるかを知ることはできません。 したがって、これを追跡するために、depthというフィールドをprocessNodeメソッドに渡します。 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. 結論

このチュートリアルでは、ジャクソンのツリーモデルを操作する一般的なAPIとシナリオについて説明しました。

そして、いつものように、これらすべての例とコードスニペットの実装はover on GitHub にあります。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。