«1. Обзор

В этом руководстве мы узнаем, как использовать библиотеку SnakeYAML для сериализации объектов Java в документы YAML и наоборот.

2. Настройка проекта

Чтобы использовать SnakeYAML в нашем проекте, мы добавим следующую зависимость Maven (последнюю версию можно найти здесь):

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.21</version>            
</dependency>

3. Точка входа

Класс Yaml является точкой входа для API:

Yaml yaml = new Yaml();

Поскольку реализация не является потокобезопасной, разные потоки должны иметь свой собственный экземпляр Yaml.

4. Загрузка документа YAML

Библиотека поддерживает загрузку документа из строки или потока ввода. Большинство примеров кода здесь будут основаны на анализе InputStream.

Давайте начнем с определения простого документа YAML и назовем файл customer.yaml:

firstName: "John"
lastName: "Doe"
age: 20

4.1. Основное использование

Теперь мы проанализируем вышеуказанный документ YAML с помощью класса Yaml:

Yaml yaml = new Yaml();
InputStream inputStream = this.getClass()
  .getClassLoader()
  .getResourceAsStream("customer.yaml");
Map<String, Object> obj = yaml.load(inputStream);
System.out.println(obj);

Приведенный выше код генерирует следующий вывод:

{firstName=John, lastName=Doe, age=20}

По умолчанию метод load() возвращает карту пример. Чтобы каждый раз запрашивать объект Map, нам потребуется заранее знать имена ключей свойств, а также нелегко перемещаться по вложенным свойствам.

4.2. Пользовательский тип

Библиотека также предоставляет способ загрузки документа в качестве пользовательского класса. Эта опция позволит легко перемещаться по данным в памяти.

Давайте определим класс Customer и попробуем снова загрузить документ:

public class Customer {

    private String firstName;
    private String lastName;
    private int age;

    // getters and setters
}

Предполагая, что документ YAML десериализован как известный тип, мы можем явно указать глобальный тег в документе.

Давайте обновим документ и сохраним его в новом файле customer_with_type.yaml:

!!com.baeldung.snakeyaml.Customer
firstName: "John"
lastName: "Doe"
age: 20

Обратите внимание на первую строку в документе, которая содержит информацию о классе, который будет использоваться при его загрузке.

Теперь мы обновим код, использованный выше, и передадим новое имя файла в качестве входных данных:

Yaml yaml = new Yaml();
InputStream inputStream = this.getClass()
 .getClassLoader()
 .getResourceAsStream("yaml/customer_with_type.yaml");
Customer customer = yaml.load(inputStream);

Метод load() теперь возвращает экземпляр типа Customer. Недостатком этого подхода является то, что тип должен быть экспортирован как библиотека, чтобы его можно было использовать там, где это необходимо.

Хотя мы могли бы использовать явный локальный тег, для которого не требуется экспортировать библиотеки.

Другой способ загрузки пользовательского типа — использование класса Constructor. Таким образом, мы можем указать корневой тип документа YAML для анализа. Давайте создадим экземпляр Constructor с типом Customer в качестве корневого типа и передадим его экземпляру Yaml.

Теперь при загрузке customer.yaml мы получим объект Customer:

Yaml yaml = new Yaml(new Constructor(Customer.class));

4.3. Неявные типы

Если для данного свойства не определен тип, библиотека автоматически преобразует значение в неявный тип.

Например:

1.0 -> Float
42 -> Integer
2009-03-30 -> Date

Давайте проверим это неявное преобразование типов на тестовом примере:

@Test
public void whenLoadYAML_thenLoadCorrectImplicitTypes() {
   Yaml yaml = new Yaml();
   Map<Object, Object> document = yaml.load("3.0: 2018-07-22");
 
   assertNotNull(document);
   assertEquals(1, document.size());
   assertTrue(document.containsKey(3.0d));   
}

4.4. Вложенные объекты и коллекции

Учитывая тип верхнего уровня, библиотека автоматически определяет типы вложенных объектов, если они не являются интерфейсом или абстрактным классом, и десериализует документ в соответствующий вложенный тип.

Давайте добавим контактную информацию и адрес в файл customer.yaml и сохраним новый файл как customer_with_contact_details_and_address.yaml.

Теперь разберем новый документ YAML:

firstName: "John"
lastName: "Doe"
age: 31
contactDetails:
   - type: "mobile"
     number: 123456789
   - type: "landline"
     number: 456786868
homeAddress:
   line: "Xyz, DEF Street"
   city: "City Y"
   state: "State Y"
   zip: 345657

Класс Customer также должен отражать эти изменения. Вот обновленный класс:

public class Customer {
    private String firstName;
    private String lastName;
    private int age;
    private List<Contact> contactDetails;
    private Address homeAddress;    
    // getters and setters
}

Давайте посмотрим, как выглядят классы Contact и Address:

public class Contact {
    private String type;
    private int number;
    // getters and setters
}
public class Address {
    private String line;
    private String city;
    private String state;
    private Integer zip;
    // getters and setters
}

@Test
public void 
  whenLoadYAMLDocumentWithTopLevelClass_thenLoadCorrectJavaObjectWithNestedObjects() {
 
    Yaml yaml = new Yaml(new Constructor(Customer.class));
    InputStream inputStream = this.getClass()
      .getClassLoader()
      .getResourceAsStream("yaml/customer_with_contact_details_and_address.yaml");
    Customer customer = yaml.load(inputStream);
 
    assertNotNull(customer);
    assertEquals("John", customer.getFirstName());
    assertEquals("Doe", customer.getLastName());
    assertEquals(31, customer.getAge());
    assertNotNull(customer.getContactDetails());
    assertEquals(2, customer.getContactDetails().size());
    
    assertEquals("mobile", customer.getContactDetails()
      .get(0)
      .getType());
    assertEquals(123456789, customer.getContactDetails()
      .get(0)
      .getNumber());
    assertEquals("landline", customer.getContactDetails()
      .get(1)
      .getType());
    assertEquals(456786868, customer.getContactDetails()
      .get(1)
      .getNumber());
    assertNotNull(customer.getHomeAddress());
    assertEquals("Xyz, DEF Street", customer.getHomeAddress()
      .getLine());
}

Теперь мы проверим Yaml#load() с заданным тестовым примером:

4.5. Типобезопасные коллекции

Когда одно или несколько свойств данного класса Java являются типобезопасными (универсальными) коллекциями, важно указать TypeDescription, чтобы определить правильный параметризованный тип.

firstName: "John"
lastName: "Doe"
age: 31
contactDetails:
   - { type: "mobile", number: 123456789}
   - { type: "landline", number: 123456789}

Давайте возьмем одного Заказчика, имеющего более одного Контакта, и попытаемся загрузить его:

Constructor constructor = new Constructor(Customer.class);
TypeDescription customTypeDescription = new TypeDescription(Customer.class);
customTypeDescription.addPropertyParameters("contactDetails", Contact.class);
constructor.addTypeDescription(customTypeDescription);
Yaml yaml = new Yaml(constructor);

Чтобы загрузить этот документ, мы можем указать TypeDescription для данного свойства в классе верхнего уровня:

4.6. Загрузка нескольких документов

Могут быть случаи, когда в одном файле находится несколько документов YAML, и мы хотим проанализировать их все. Класс Yaml предоставляет метод loadAll() для такого анализа.

«По умолчанию метод возвращает экземпляр Iterable\u003cObject\u003e, где каждый объект имеет тип Map\u003cString, Object\u003e. Если требуется пользовательский тип, мы можем использовать экземпляр Constructor, как обсуждалось выше.

---
firstName: "John"
lastName: "Doe"
age: 20
---
firstName: "Jack"
lastName: "Jones"
age: 25

Рассмотрим следующие документы в одном файле:

@Test
public void whenLoadMultipleYAMLDocuments_thenLoadCorrectJavaObjects() {
    Yaml yaml = new Yaml(new Constructor(Customer.class));
    InputStream inputStream = this.getClass()
      .getClassLoader()
      .getResourceAsStream("yaml/customers.yaml");

    int count = 0;
    for (Object object : yaml.loadAll(inputStream)) {
        count++;
        assertTrue(object instanceof Customer);
    }
    assertEquals(2,count);
}

Мы можем разобрать вышеуказанное с помощью метода loadAll(), как показано в приведенном ниже примере кода:

5. Создание дампа документов YAML ~~ ~ Библиотека также предоставляет метод для создания дампа заданного объекта Java в документ YAML. Вывод может быть строкой или указанным файлом/потоком.

5.1. Основное использование

Мы начнем с простого примера выгрузки экземпляра Map\u003cString, Object\u003e в документ YAML (String):

@Test
public void whenDumpMap_thenGenerateCorrectYAML() {
    Map<String, Object> data = new LinkedHashMap<String, Object>();
    data.put("name", "Silenthand Olleander");
    data.put("race", "Human");
    data.put("traits", new String[] { "ONE_HAND", "ONE_EYE" });
    Yaml yaml = new Yaml();
    StringWriter writer = new StringWriter();
    yaml.dump(data, writer);
    String expectedYaml = "name: Silenthand Olleander\nrace: Human\ntraits: [ONE_HAND, ONE_EYE]\n";

    assertEquals(expectedYaml, writer.toString());
}

Вышеприведенный код выводит следующий результат (обратите внимание, что использование экземпляр LinkedHashMap сохраняет порядок выходных данных):

name: Silenthand Olleander
race: Human
traits: [ONE_HAND, ONE_EYE]

5.2. Пользовательские объекты Java

Мы также можем выбрать вывод пользовательских типов Java в выходной поток. Это, однако, добавит глобальный явный тег в выходной документ:

@Test
public void whenDumpACustomType_thenGenerateCorrectYAML() {
    Customer customer = new Customer();
    customer.setAge(45);
    customer.setFirstName("Greg");
    customer.setLastName("McDowell");
    Yaml yaml = new Yaml();
    StringWriter writer = new StringWriter();
    yaml.dump(customer, writer);        
    String expectedYaml = "!!com.baeldung.snakeyaml.Customer {age: 45, contactDetails: null, firstName: Greg,\n  homeAddress: null, lastName: McDowell}\n";

    assertEquals(expectedYaml, writer.toString());
}

Используя описанный выше подход, мы по-прежнему выгружаем информацию тега в документ YAML.

Это означает, что мы должны экспортировать наш класс как библиотеку для любого потребителя, который его десериализует. Чтобы избежать имени тега в выходном файле, мы можем использовать метод dumpAs(), предоставляемый библиотекой.

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

yaml.dumpAs(customer, Tag.MAP, null);

6. Заключение

В этой статье показано использование библиотеки SnakeYAML для сериализации объектов Java в YAML и наоборот.

Все примеры можно найти в проекте GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.