ジャクソンでのツリーモデルノードの操作
1. 概要
このチュートリアルでは、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 toJsonNodeの記事で詳しく説明されています。 詳細については、それを参照してください。
2.3. オブジェクトから変換する
ObjectMapperのvalueToTree(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, "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);
セクション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", "baeldung.com");
NodeBean toValue = mapper.treeToValue(node, NodeBean.class);
assertEquals(2016, toValue.getId());
assertEquals("baeldung.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–ノードの反復で、すべてのノードの反復の例を見ることができます。
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. ノードの編集
ObjectNode インスタンスは、 set(String fieldName、JsonNode value)メソッドを呼び出すことで変更できます。
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. ノードの削除
ノードは、その親ノードで remove(String fieldName)APIを呼び出すことで削除できます。
JsonNode removedNode = locatedNode.remove(fieldName);
複数のノードを一度に削除するために、次のパラメータを使用してオーバーロードされたメソッドを呼び出すことができます。 コレクション
ObjectNode locatedNode = locatedNode.remove(fieldNames);
極端な場合、特定のノードのすべてのサブノードを削除する場合は、 the 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メソッドにフラグを渡します。
最後に、各オブジェクトノードのすべての子ノードを反復処理します。 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<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;
}
}
ノードからは、祖先がいくつあるかを知ることはできません。
したがって、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. 結論
この記事では、Jacksonでツリーモデルを操作する際の一般的なAPIとシナリオについて説明しました。
そしていつものように、これらすべての例とコードスニペットの実装はGitHubにあります。