Trabalhando com nós de modelo de árvore em Jackson
1. Visão geral
Este tutorial se concentrará em trabalhar comtree model nodes in Jackson.
UsaremosJsonNode para várias conversões, bem como adicionar, modificar e remover nós.
2. Criando um Nó
A primeira etapa na criação de um nó é instanciar um objetoObjectMapper usando o construtor padrão:
ObjectMapper mapper = new ObjectMapper();
Uma vez que a criação de um objetoObjectMapper é cara, é recomendado que o mesmo seja reutilizado para várias operações.
A seguir, temos três maneiras diferentes de criar um nó de árvore, uma vez que temos nossoObjectMapper.
2.1. Construir um nó do zero
A maneira mais comum de criar um nó do nada é a seguinte:
JsonNode node = mapper.createObjectNode();
Alternativamente, também podemos criar um nó por meio doJsonNodeFactory:
JsonNode node = JsonNodeFactory.instance.objectNode();
2.2. Analisar de uma fonte JSON
Esse método é bem abordado no artigoJackson – Marshall String to JsonNode. Consulte-o se precisar de mais informações.
2.3. Converter de um objeto
Um nó pode ser convertido de um objeto Java chamando o métodovalueToTree(Object fromValue) noObjectMapper:
JsonNode node = mapper.valueToTree(fromValue);
A APIconvertValue também é útil aqui:
JsonNode node = mapper.convertValue(fromValue, JsonNode.class);
Vamos ver como isso funciona na prática. Suponha que temos uma classe chamadaNodeBean:
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
}
Vamos escrever um teste para garantir que a conversão aconteça corretamente:
@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. Transformando um Nó
3.1. Escreva como JSON
O método básico para transformar um nó da árvore em uma sequência JSON é o seguinte:
mapper.writeValue(destination, node);
onde o destino pode ser umFile, umOutputStream ou umWriter. Ao reutilizar a classeNodeBean declarada na seção 2.3, um teste garante que esse método funcione conforme o esperado:
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. Converter em objeto
A maneira mais conveniente de converterJsonNode em um objeto Java é a APItreeToValue:
NodeBean toValue = mapper.treeToValue(node, NodeBean.class);
Qual é funcionalmente equivalente a:
NodeBean toValue = mapper.convertValue(node, NodeBean.class)
Também podemos fazer isso por meio de um fluxo de token:
JsonParser parser = mapper.treeAsTokens(node);
NodeBean toValue = mapper.readValue(parser, NodeBean.class);
Por fim, vamos implementar um teste que verifica o processo de conversão:
@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. Manipulando nós de árvore
Os seguintes elementos JSON, contidos em um arquivo denominadoexample.json, são usados como uma estrutura de base para as ações discutidas nesta seção a serem realizadas:
{
"name":
{
"first": "Tatu",
"last": "Saloranta"
},
"title": "Jackson founder",
"company": "FasterXML"
}
Este arquivo JSON, localizado no caminho de classe, é analisado em uma árvore de 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;
}
}
Observe que a raiz da árvore será usada ao ilustrar operações em nós nas subseções a seguir.
4.1. Localizando um Nó
Antes de trabalhar em qualquer nó, a primeira coisa que precisamos fazer é localizá-lo e atribuí-lo a uma variável.
Se o caminho para o nó for conhecido de antemão, isso é muito fácil de fazer. Por exemplo, digamos que queremos um nó denominadolast, que está sob o nóname:
JsonNode locatedNode = rootNode.path("name").path("last");
Alternativamente, as APIsget ouwith também podem ser usadas em vez depath.
Se o caminho não for conhecido, a pesquisa, é claro, se tornará mais complexa e iterativa.
Podemos ver um exemplo de iteração sobre todos os nós em5. Iterating Over the Nodes
4.2. Adicionando um Novo Nó
Um nó pode ser adicionado como filho de outro nó, da seguinte maneira:
ObjectNode newNode = ((ObjectNode) locatedNode).put(fieldName, value);
Muitas variantes sobrecarregadas deput podem ser usadas para adicionar novos nós de diferentes tipos de valor.
Muitos outros métodos semelhantes também estão disponíveis, incluindoputArray,putObject,PutPOJO,putRawValueeputNull.
Finalmente - vamos dar uma olhada em um exemplo - onde adicionamos uma estrutura inteira ao nó raiz da árvore:
"address":
{
"city": "Seattle",
"state": "Washington",
"country": "United States"
}
Aqui está o teste completo passando por todas essas operações e verificando os 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. Editando um Nó
Uma instânciaObjectNode pode ser modificada invocando o métodoset(String fieldName, JsonNode value):
JsonNode locatedNode = locatedNode.set(fieldName, value);
Resultados semelhantes podem ser obtidos usando os métodosreplace ousetAll em objetos do mesmo tipo.
Para verificar se o método funciona conforme o esperado, vamos alterar o valor do camponame no nó raiz de um objeto defirstelast para outro consistindo de apenasnickcampo s em um teste:
@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. Removendo um Nó
Um nó pode ser removido chamando a APIremove(String fieldName) em seu nó pai:
JsonNode removedNode = locatedNode.remove(fieldName);
Para remover vários nós de uma vez, podemos invocar um método sobrecarregado com o parâmetro do tipoCollection<String>, que retorna o nó pai em vez daquele a ser removido:
ObjectNode locatedNode = locatedNode.remove(fieldNames);
No caso extremo, quando queremos excluir todos os subnós de um determinado nó–, a APIremoveAll é útil.
O teste a seguir se concentrará no primeiro método mencionado acima - que é o cenário mais comum:
@Test
public void givenANode_whenRemovingFromATree_thenCorrect() throws IOException {
JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).remove("company");
assertTrue(rootNode.path("company").isMissingNode());
}
5. Iterando sobre os nós
Vamos iterar todos os nós em um documento JSON e reformatá-los em YAML. JSON possui três tipos de nó, que são Valor, Objeto e Matriz.
Então, vamos garantir que nossos dados de amostra tenham todos os três tipos diferentes, adicionando umArray:
{
"name":
{
"first": "Tatu",
"last": "Saloranta"
},
"title": "Jackson founder",
"company": "FasterXML",
"pets" : [
{
"type": "dog",
"number": 1
},
{
"type": "fish",
"number": 50
}
]
}
Agora, vamos ver o YAML que queremos produzir:
name:
first: Tatu
last: Saloranta
title: Jackson founder
company: FasterXML
pets:
- type: dog
number: 1
- type: fish
number: 50
Sabemos que os nós JSON têm uma estrutura hierárquica em árvore. Portanto, a maneira mais fácil de percorrer todo o documento JSON é começar do topo e percorrer todos os nós filhos.
Vamos passar o nó raiz para um método recursivo. O método então se chamará com cada filho do nó fornecido.
5.1. Testando a Iteração
Começaremos criando um teste simples que verifica se podemos converter JSON em YAML com sucesso.
Nosso teste fornece o nó raiz do documento JSON para nosso métodotoYaml e afirma que o valor retornado é o que esperamos:
@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. Lidando com Diferentes Tipos de Nó
Precisamos lidar com diferentes tipos de nós de maneira um pouco diferente. Faremos isso em nosso métodoprocessNode:
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);
}
}
Primeiro, vamos considerar um nó de valor. Simplesmente chamamos o métodoasText do nó para obter uma representaçãoString do valor.
A seguir, vamos dar uma olhada em um nó Array. Cada item dentro do nó Array é ele mesmo umJsonNode, então iteramos sobre o Array e passamos cada nó para o métodoappendNodeToYaml. Também precisamos saber que esses nós fazem parte de uma matriz.
Infelizmente, o próprio nó não contém nada que nos diga isso, então vamos passar um sinalizador para nosso métodoappendNodeToYaml.
Finalmente, queremos iterar sobre todos os nós filhos de cada nó Objeto. Uma opção é usarJsonNode.elements. No entanto, não podemos determinar o nome do campo de um elemento, pois ele contém apenas o valor do campo:
Object {"first": "Tatu", "last": "Saloranta"}
Value "Jackson Founder"
Value "FasterXML"
Array [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]
Em vez disso, usaremosJsonNode.fields, pois isso nos dá acesso ao nome e ao valor do campo:
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}]
Para cada campo, adicionamos o nome do campo à saída. Em seguida, processe o valor como um nó filho, passando-o para o métodoprocessNode:
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;
}
}
Não podemos dizer a partir do nó quantos ancestrais ele tem. Então, passamos um campo chamado profundidade no métodoprocessNode para manter o controle disso. Nós incrementamos esse valor sempre que obtemos um nó filho para podermos recuar corretamente os campos em nossa saída 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(": ");
}
Agora que temos todo o código para iterar sobre os nós e criar a saída YAML, podemos executar nosso teste para mostrar que funciona.
6. Conclusão
Este tutorial abordou as APIs e cenários comuns de trabalho com um modelo de árvore em Jackson.
E, como sempre, a implementação de todos esses exemplos e trechos de código podem ser encontrados emover on GitHub – este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.