Lavorare con i nodi del modello di albero a Jackson

1. Panoramica

Questo tutorial si concentrerà sul lavoro con i nodi del modello di albero in Jackson .

Useremo JsonNode per varie conversioni, nonché per aggiungere, modificare e rimuovere nodi.

2. Creazione di un nodo

Il primo passaggio nella creazione di un nodo consiste nell'istanziare un oggetto ObjectMapper utilizzando il costruttore predefinito:

ObjectMapper mapper = new ObjectMapper();

Poiché la creazione di un oggetto ObjectMapper è costosa, si consiglia di riutilizzare lo stesso per più operazioni.

Successivamente, abbiamo tre diversi modi per creare un nodo ad albero una volta che abbiamo il nostro ObjectMapper .

2.1. Costruisci un nodo da zero

Il modo più comune per creare un nodo dal nulla è il seguente:

JsonNode node = mapper.createObjectNode();

In alternativa, possiamo anche creare un nodo tramite JsonNodeFactory :

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. Analizza da un'origine JSON

Questo metodo è ben trattato nell'articolo Jackson - Marshall String to JsonNode. Si prega di fare riferimento ad esso se avete bisogno di maggiori informazioni.

2.3. Converti da un oggetto

Un nodo può essere convertito da un oggetto Java chiamando il metodo valueToTree (Object fromValue) su ObjectMapper :

JsonNode node = mapper.valueToTree(fromValue);

Anche l' API convertValue è utile qui:

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

Vediamo come funziona in pratica. Supponiamo di avere una classe chiamata 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 }

Scriviamo un test che assicuri che la conversione avvenga correttamente:

@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. Trasformare un nodo

3.1. Scrivi come JSON

Il metodo di base per trasformare un nodo della struttura ad albero in una stringa JSON è il seguente:

mapper.writeValue(destination, node);

dove la destinazione può essere un File , un OutputStream o un Writer .

Riutilizzando la classe NodeBean dichiarata nella sezione 2.3, un test si assicura che questo metodo funzioni come previsto:

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. Converti in un oggetto

Il modo più conveniente per convertire un JsonNode in un oggetto Java è l' API treeToValue :

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

Che è funzionalmente equivalente a:

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

Possiamo anche farlo tramite un flusso di token:

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

Infine, implementiamo un test che verifica il processo di conversione:

@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. Manipolazione dei nodi dell'albero

I seguenti elementi JSON, contenuti in un file denominato example.json , vengono utilizzati come struttura di base per le azioni discusse in questa sezione da intraprendere:

{ "name": { "first": "Tatu", "last": "Saloranta" }, "title": "Jackson founder", "company": "FasterXML" }

Questo file JSON, situato nel classpath, viene analizzato in un albero del modello:

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

Si noti che la radice dell'albero verrà utilizzata per illustrare le operazioni sui nodi nelle seguenti sottosezioni.

4.1. Individuazione di un nodo

Prima di lavorare su qualsiasi nodo, la prima cosa che dobbiamo fare è individuarlo e assegnarlo a una variabile.

Se il percorso del nodo è noto in anticipo, è abbastanza facile da fare. Ad esempio, supponiamo di volere un nodo denominato per ultimo , che si trova sotto il nome nodo:

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

In alternativa, è possibile utilizzare anche le API get o with al posto di path .

Se il percorso non è noto, la ricerca, ovviamente, diventerà più complessa e iterativa.

Possiamo vedere un esempio di iterazione su tutti i nodi in 5. Iterazione sui nodi

4.2. Aggiunta di un nuovo nodo

Un nodo può essere aggiunto come figlio di un altro nodo come segue:

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

Molte varianti di put sovraccaricate possono essere utilizzate per aggiungere nuovi nodi di diversi tipi di valore.

Sono disponibili anche molti altri metodi simili, inclusi putArray , putObject , PutPOJO , putRawValue e putNull .

Infine - diamo un'occhiata a un esempio - in cui aggiungiamo un'intera struttura al nodo radice dell'albero:

"address": { "city": "Seattle", "state": "Washington", "country": "United States" }

Ecco il test completo che attraversa tutte queste operazioni e verifica i risultati:

@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. Modifica di un nodo

Un ObjectNode esempio può essere modificato richiamando set (String fieldName, valore JsonNode) Metodo:

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

Similar results might be achieved by using replace or setAll methods on objects of the same type.

To verify that the method works as expected, we will change the value of the field name under root node from an object of first and last into another one consisting of only nick field in a test:

@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

A node can be removed by calling the remove(String fieldName) API on its parent node:

JsonNode removedNode = locatedNode.remove(fieldName);

In order to remove multiple nodes at once, we can invoke an overloaded method with the parameter of Collection type, which returns the parent node instead of the one to be removed:

ObjectNode locatedNode = locatedNode.remove(fieldNames);

In the extreme case when we want to delete all subnodes of a given node the removeAll API comes in handy.

The following test will focus on the first method mentioned above – which is the most common scenario:

@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 node, 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 node 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.isArray()) { 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 as 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 as 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. 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
    
      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; } }
    

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. 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. Conclusion

This tutorial covered the common APIs and scenarios of working with a tree model in Jackson.

E, come sempre, l'implementazione di tutti questi esempi e frammenti di codice può essere trovata su GitHub : questo è un progetto basato su Maven, quindi dovrebbe essere facile da importare ed eseguire così com'è.