Trabajar con nodos de modelo de árbol en Jackson

1. Información general

Este tutorial se centrará en trabajar con nodos de modelo de árbol en Jackson .

Usaremos JsonNode para varias conversiones, así como para agregar, modificar y eliminar nodos.

2. Crear un nodo

El primer paso en la creación de un nodo es crear una instancia de un objeto ObjectMapper utilizando el constructor predeterminado:

ObjectMapper mapper = new ObjectMapper();

Dado que la creación de un objeto ObjectMapper es costosa, se recomienda que el mismo se reutilice para múltiples operaciones.

A continuación, tenemos tres formas diferentes de crear un nodo de árbol una vez que tenemos nuestro ObjectMapper .

2.1. Construir un nodo desde cero

La forma más común de crear un nodo de la nada es la siguiente:

JsonNode node = mapper.createObjectNode();

Alternativamente, también podemos crear un nodo a través de JsonNodeFactory :

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. Analizar desde una fuente JSON

Este método está bien cubierto en el artículo Jackson-Marshall String to JsonNode. Consúltelo si necesita más información.

2.3. Convertir de un objeto

Un nodo puede convertirse de un objeto Java llamando al método valueToTree (Object fromValue) en ObjectMapper :

JsonNode node = mapper.valueToTree(fromValue);

La API convertValue también es útil aquí:

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

Veamos cómo funciona en la práctica. Supongamos que tenemos una clase llamada 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 }

Escribamos una prueba que asegure que la conversión ocurre correctamente:

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

3.1. Escriba como JSON

El método básico para transformar un nodo de árbol en una cadena JSON es el siguiente:

mapper.writeValue(destination, node);

donde el destino puede ser un archivo , un OutputStream o un escritor .

Al reutilizar la clase NodeBean declarada en la sección 2.3, una prueba asegura que este método funcione como se esperaba:

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 objeto

La forma más conveniente de convertir un JsonNode en un objeto Java es la API treeToValue :

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

Que es funcionalmente equivalente a:

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

También podemos hacerlo a través de una secuencia de tokens:

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

Finalmente, implementemos una prueba que verifique el proceso de conversión:

@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. Manipulación de nodos de árboles

Los siguientes elementos JSON, contenidos en un archivo llamado example.json , se utilizan como estructura base para las acciones que se describen en esta sección:

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

Este archivo JSON, ubicado en la ruta de clases, se analiza en un árbol modelo:

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

Tenga en cuenta que la raíz del árbol se utilizará al ilustrar las operaciones en los nodos en las siguientes subsecciones.

4.1. Localizar un nodo

Antes de trabajar en cualquier nodo, lo primero que debemos hacer es ubicarlo y asignarlo a una variable.

Si la ruta al nodo se conoce de antemano, es bastante fácil de hacer. Por ejemplo, digamos que queremos un nodo llamado last , que está bajo el nombre node:

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

Alternativamente, las API get o with también se pueden usar en lugar de la ruta .

Si no se conoce la ruta, la búsqueda, por supuesto, se volverá más compleja e iterativa.

Podemos ver un ejemplo de iteración sobre todos los nodos en 5. Iteración sobre los nodos

4.2. Agregar un nodo nuevo

Se puede agregar un nodo como hijo de otro nodo de la siguiente manera:

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

Se pueden usar muchas variantes sobrecargadas de put para agregar nuevos nodos de diferentes tipos de valor.

También están disponibles muchos otros métodos similares, incluidos putArray , putObject , PutPOJO , putRawValue y putNull .

Finalmente, echemos un vistazo a un ejemplo, donde agregamos una estructura completa al nodo raíz del árbol:

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

Aquí está la prueba completa que pasa por todas estas operaciones y verifica los resultados:

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

Una instancia de ObjectNode se puede modificar invocando el método set (String fieldName, valor JsonNode) :

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

Se pueden lograr resultados similares utilizando los métodos replace o setAll en objetos del mismo tipo.

Para verificar que el método funciona como se esperaba, cambiaremos el valor del nombre del campo bajo el nodo raíz de un objeto de primero y último a otro que consiste solo en un campo de nick en una prueba:

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

Un nodo se puede eliminar llamando a la API remove (String fieldName) en su nodo principal:

JsonNode removedNode = locatedNode.remove(fieldName);

Para eliminar varios nodos a la vez, podemos invocar un método sobrecargado con el parámetro de tipo Colección , que devuelve el nodo principal en lugar del que se eliminará:

ObjectNode locatedNode = locatedNode.remove(fieldNames);

En el caso extremo en el que queremos eliminar todos los subnodos de un nodo determinado , la API removeAll resulta útil.

La siguiente prueba se centrará en el primer método mencionado anteriormente, que es el escenario más común:

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

5. Iterando sobre los nodos

Vamos a iterar sobre todos los nodos en un documento JSON y reformatearlos en YAML. JSON tiene tres tipos de nodos, que son Valor, Objeto y Matriz.

Entonces, asegurémonos de que nuestros datos de muestra tengan los tres tipos diferentes agregando una matriz:

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

Ahora, veamos el YAML que queremos producir:

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

Sabemos que los nodos JSON tienen una estructura de árbol jerárquica. Entonces, la forma más fácil de iterar sobre todo el documento JSON es comenzar desde la parte superior y avanzar a través de todos los nodos secundarios.

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.

Y, como siempre, la implementación de todos estos ejemplos y fragmentos de código se puede encontrar en GitHub : este es un proyecto basado en Maven, por lo que debería ser fácil de importar y ejecutar tal como está.