1. 概述

本文我们将学习如何使用 SnakeYAML 库 序列化Java对象为YAML文档,以及反向序列化

2. Maven 依赖

首先添加 SnakeYAML 依赖, 最新版本请查看这里:

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

3. 初始化

初始化 Yaml 对象:

Yaml yaml = new Yaml();

由于其实现不是线程安全的,因此不同的线程必须有它们自己的 Yaml 实例。

4. 解析 YAML 文档

SnakeYAML 支持从 String 或 InputStream 中加载文档。

假设有下面的 YAML文档,customer.yaml:

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

4.1. 基础用法

下面示例从 ClassPath 目录中加载 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 实例,下节我们学习如何映射为Java Bean。

4.2. 自定义类型

The library also provides a way to load the document as a custom class. This option would allow easy traversal of data in memory.

定义要映射的实体类,Customer class:

public class Customer {

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

    // getters and setters
}

Assuming the YAML document to be deserialized as a known type, we can specify an explicit global tag in the document.

Let’s update the document and store it in a new file customer_with_type.yaml:

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

Note the first line in the document, which holds the info about the class to be used when loading it.

Now we’ll update the code used above, and pass the new file name as input:

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

The load() method now returns an instance of Customer type*.* The drawback to this approach is that the type has to be exported as a library in order to be used where needed.

Although, we could use the explicit local tag for which we aren’t required to export libraries.

Another way of loading a custom type is by using the Constructor class. This way we can specify the root type for a YAML document to be parsed. Let us create a Constructor instance with the Customer type as root type and pass it to the Yaml instance.

Now on loading the customer.yaml, we’ll get the Customer object:

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

4.3. 隐式类型

In case there’s no type defined for a given property, the library automatically converts the value to an implicit type.

For example:

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

Let’s test this implicit type conversion using a test case:

@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. 嵌套对象和集合

Given a top-level type, the library automatically detects the types of nested objects, unless they’re an interface or an abstract class, and deserializes the document into the relevant nested type.

Let’s add Contact and Address details to the customer.yaml, and save the new file as customer_with_contact_details_and_address.yaml. 

Now we’ll parse the new YAML document:

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 class should also reflect these changes. Here’s the updated class:

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

Let’s see how Contact and Address classes look like:

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
}

Now we’ll test the Yaml#load() with the given test case:

@Test
public void 
  whenLoadYAMLDocumentWithTopLevelClass_thenLoadCorrectJavaObjectWithNestedObjects() {
 
    Yaml yaml = new Yaml(new Constructor(Customer.class, new LoaderOptions()));
    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());
}

4.5. 类型安全的集合

When one or more properties of a given Java class are type-safe (generic) collections, then it’s important to specify the TypeDescription so that the correct parameterized type is identified.

Let’s take one Customer having more than one Contact, and try to load it:

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

In order to load this document, we can specify the TypeDescription for the given property on the top level class:

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

4.6. 加载多个文档

There could be cases where, in a single File there are several YAML documents, and we want to parse all of them. The Yaml class provides a loadAll() method to do such type of parsing.

By default, the method returns an instance of Iterable where each object is of type Map<String, Object>. If a custom type is desired then we can use the Constructor instance as discussed above*.* 

Consider the following documents in a single file:

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

We can parse the above using the loadAll() method as shown in the below code sample:

@Test
public void whenLoadMultipleYAMLDocuments_thenLoadCorrectJavaObjects() {
    Yaml yaml = new Yaml(new Constructor(Customer.class, new LoaderOptions()));
    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);
}

5. 序列化为YAML文档

本节我们学习如何将 Java 对象序列化为YAML字符串

5.1. 基础用法

We’ll start with a simple example of dumping an instance of Map<String, Object> to a YAML document (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());
}

The above code produces the following output (note that using an instance of LinkedHashMap preserves the order of the output data):

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

5.2. 自定义Java对象

We can also choose to dump custom Java types into an output stream. This will, however, add the global explicit tag to the output document:

@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());
}

With the above approach, we’re still dumping the tag information in YAML document.

This means we have to export our class as a library for any consumer who is deserializing it. In order to avoid the tag name in the output file, we can use the dumpAs() method provided by the library.

So in the above code, we could tweak the following to remove the tag:

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

6. 总结

This article illustrated usages of SnakeYAML library to serialize Java objects to YAML and vice versa.

All of the examples can be found in the GitHub project – this is a Maven based project, so it should be easy to import and run as it is.