Работа с възли на дървесни модели в Джаксън

1. Общ преглед

Този урок ще се фокусира върху работата с възли на дървесни модели в Джаксън .

Ще използваме JsonNode за различни преобразувания, както и за добавяне, модифициране и премахване на възли.

2. Създаване на възел

Първата стъпка в създаването на възел е да се създаде екземпляр на обект ObjectMapper с помощта на конструктора по подразбиране:

ObjectMapper mapper = new ObjectMapper();

Тъй като създаването на обект ObjectMapper е скъпо, препоръчително е един и същ да се използва повторно за множество операции.

След това имаме три различни начина да създадем дървесен възел, след като имаме ObjectMapper .

2.1. Изградете възел от нулата

Най-често срещаният начин за създаване на възел от нищо е следният:

JsonNode node = mapper.createObjectNode();

Друга възможност е да създадем възел чрез JsonNodeFactory :

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. Анализ от JSON източник

Този метод е добре описан в статията Jackson - Marshall String to JsonNode. Моля, обърнете се към него, ако имате нужда от повече информация.

2.3. Конвертиране от обект

Възел може да бъде преобразуван от Java обект чрез извикване на метода valueToTree (Object fromValue) в ObjectMapper :

JsonNode node = mapper.valueToTree(fromValue);

В convertValue API също е полезно тук:

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 низ е следният:

mapper.writeValue(destination, node);

където дестинацията може да бъде File , OutputStream или Writer .

Чрез повторно използване на класа NodeBean, деклариран в раздел 2.3, тест гарантира, че този метод работи както се очаква:

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 обект е API на treeToValue :

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. Манипулиране на дървесни възли

Следните JSON елементи, съдържащи се във файл с име example.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. Намиране на възел

Преди да работим върху който и да е възел, първото нещо, което трябва да направим, е да го намерим и присвоим на променлива.

Ако пътят до възела е известен предварително, това е доста лесно да се направи. Например, да кажем, че искаме възел с име последен , който е под името възел:

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

Алтернативно, get или с API може също да се използва вместо път .

Ако пътят не е известен, търсенето, разбира се, ще стане по-сложно и итеративно.

Можем да видим пример за итерация върху всички възли в 5. Итериране над възлите

4.2. Добавяне на нов възел

Възел може да бъде добавен като дете на друг възел, както следва:

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

Много претоварени варианти на пут могат да се използват за добавяне на нови възли от различни типове стойности.

Предлагат се и много други подобни методи, включително 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 може да бъде модифициран чрез извикване на метод (String fieldName, JsonNode стойност) метод:

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

Подобни резултати могат да бъдат постигнати чрез използване на методи replace или setAll за обекти от същия тип.

За да проверим дали методът работи както се очаква, ще променим стойността на името на полето под коренния възел от обект от 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. Премахване на възел

Възел може да бъде премахнат чрез извикване на API за премахване (String fieldName) на неговия родителски възел:

JsonNode removedNode = locatedNode.remove(fieldName);

За да премахнем няколко възела наведнъж, можем да извикаме претоварен метод с параметъра Тип на колекция , който връща родителския възел вместо този, който трябва да бъде премахнат:

ObjectNode locatedNode = locatedNode.remove(fieldNames);

В случай на екстремни, когато искате да изтриете всички subnodes на даден възел - на removeAll API е по-удобен.

Следващият тест ще се фокусира върху първия метод, споменат по-горе - който е най-често срещаният сценарий:

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

Така че, нека се уверим, че нашите примерни данни имат и трите различни типа, като добавим масив:

{ "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 представяне на стойността.

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.

И както винаги, изпълнението на всички тези примери и кодови фрагменти може да бъде намерено в над на GitHub - това е проект, базиран на Maven, така че трябва да е лесно да се импортира и да се изпълнява както е.