Utilisation des nœuds de modèle d'arbre dans Jackson

1. Vue d'ensemble

Ce didacticiel se concentrera sur l'utilisation des nœuds de modèle d'arbre dans Jackson .

Nous utiliserons JsonNode pour diverses conversions ainsi que pour ajouter, modifier et supprimer des nœuds.

2. Création d'un nœud

La première étape de la création d'un nœud consiste à instancier un objet ObjectMapper à l'aide du constructeur par défaut:

ObjectMapper mapper = new ObjectMapper();

La création d'un objet ObjectMapper étant coûteuse, il est recommandé de réutiliser le même objet pour plusieurs opérations.

Ensuite, nous avons trois façons différentes de créer un nœud d'arbre une fois que nous avons notre ObjectMapper .

2.1. Construire un nœud à partir de zéro

La façon la plus courante de créer un nœud à partir de rien est la suivante:

JsonNode node = mapper.createObjectNode();

Alternativement, nous pouvons également créer un nœud via JsonNodeFactory :

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. Analyser à partir d'une source JSON

Cette méthode est bien couverte dans l'article Jackson - Marshall String to JsonNode. Veuillez vous y référer si vous avez besoin de plus d'informations.

2.3. Convertir à partir d'un objet

Un nœud peut être converti à partir d'un objet Java en appelant la méthode valueToTree (Object fromValue) sur l' ObjectMapper :

JsonNode node = mapper.valueToTree(fromValue);

L' API convertValue est également utile ici:

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

Voyons comment cela fonctionne dans la pratique. Supposons que nous ayons une classe nommée 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 }

Écrivons un test qui s'assure que la conversion se déroule correctement:

@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. Transformer un nœud

3.1. Écrire en JSON

La méthode de base pour transformer un nœud d'arbre en chaîne JSON est la suivante:

mapper.writeValue(destination, node);

où la destination peut être un fichier , un OutputStream ou un Writer .

En réutilisant la classe NodeBean déclarée dans la section 2.3, un test s'assure que cette méthode fonctionne comme prévu:

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. Convertir en objet

Le moyen le plus pratique de convertir un JsonNode en objet Java est l' API treeToValue :

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

Ce qui est fonctionnellement équivalent à:

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

Nous pouvons également le faire via un jeton:

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

Enfin, implémentons un test qui vérifie le processus de conversion:

@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. Manipulation des nœuds d'arbre

Les éléments JSON suivants, contenus dans un fichier nommé example.json , sont utilisés comme structure de base pour les actions décrites dans cette section à entreprendre:

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

Ce fichier JSON, situé sur le classpath, est analysé dans une arborescence de modèle:

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

Notez que la racine de l'arborescence sera utilisée pour illustrer les opérations sur les nœuds dans les sous-sections suivantes.

4.1. Localisation d'un nœud

Avant de travailler sur un nœud, la première chose à faire est de le localiser et de l'affecter à une variable.

Si le chemin d'accès au nœud est connu à l'avance, c'est assez facile à faire. Par exemple, disons que nous voulons un nœud nommé en dernier , qui se trouve sous le nœud de nom :

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

Alternativement, les API get ou with peuvent également être utilisées à la place de path .

Si le chemin n'est pas connu, la recherche deviendra bien sûr plus complexe et itérative.

Nous pouvons voir un exemple d'itération sur tous les nœuds en 5. Itération sur les nœuds

4.2. Ajout d'un nouveau nœud

Un nœud peut être ajouté en tant qu'enfant d'un autre nœud comme suit:

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

De nombreuses variantes surchargées de put peuvent être utilisées pour ajouter de nouveaux nœuds de différents types de valeur.

De nombreuses autres méthodes similaires sont également disponibles, notamment putArray , putObject , PutPOJO , putRawValue et putNull .

Enfin - regardons un exemple - où nous ajoutons une structure entière au nœud racine de l'arbre:

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

Voici le test complet passant par toutes ces opérations et vérifiant les résultats:

@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. Modifier un nœud

Une instance ObjectNode peut être modifiée en appelant la méthode set (String fieldName, JsonNode value) :

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

Des résultats similaires peuvent être obtenus en utilisant les méthodes replace ou setAll sur des objets du même type.

Pour vérifier que la méthode fonctionne comme prévu, nous allons changer la valeur du nom de champ sous le nœud racine d'un objet de premier et dernier en un autre composé uniquement de champ nick dans un 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. Suppression d'un nœud

Un nœud peut être supprimé en appelant l' API remove (String fieldName) sur son nœud parent:

JsonNode removedNode = locatedNode.remove(fieldName);

Afin de supprimer plusieurs nœuds à la fois, nous pouvons invoquer une méthode surchargée avec le paramètre de type Collection , qui retourne le nœud parent au lieu de celui à supprimer:

ObjectNode locatedNode = locatedNode.remove(fieldNames);

Dans le cas extrême où nous voulons supprimer tous les sous-nœuds d'un nœud donné - l' API removeAll est pratique.

Le test suivant se concentrera sur la première méthode mentionnée ci-dessus - qui est le scénario le plus courant:

@Test public void givenANode_whenRemovingFromATree_thenCorrect() throws IOException { JsonNode rootNode = ExampleStructure.getExampleRoot(); ((ObjectNode) rootNode).remove("company"); assertTrue(rootNode.path("company").isMissingNode()); }

5. Itération sur les nœuds

Répétons tous les nœuds d'un document JSON et reformatons-les en YAML. JSON a trois types de nœuds, qui sont Value, Object et Array.

Alors, assurons-nous que nos exemples de données ont les trois types différents en ajoutant un tableau:

{ "name": { "first": "Tatu", "last": "Saloranta" }, "title": "Jackson founder", "company": "FasterXML", "pets" : [ { "type": "dog", "number": 1 }, { "type": "fish", "number": 50 } ] }

Voyons maintenant le YAML que nous voulons produire:

name: first: Tatu last: Saloranta title: Jackson founder company: FasterXML pets: - type: dog number: 1 - type: fish number: 50

Nous savons que les nœuds JSON ont une structure arborescente hiérarchique. Ainsi, le moyen le plus simple d'itérer sur l'ensemble du document JSON est de commencer par le haut et de parcourir tous les nœuds enfants.

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.

Et, comme toujours, la mise en œuvre de tous ces exemples et extraits de code peut être trouvée dans over sur GitHub - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.