1. Overview
In this tutorial, we’ll learn how to use SnakeYAML library to serialize Java objects to YAML documents and vice versa.
2. Project Setup
In order to use SnakeYAML in our project, we’ll add the following Maven dependency (the latest version can be found here):
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
3. Entry Point
The Yaml class is the entry point for the API:
Yaml yaml = new Yaml();
Since the implementation is not thread safe, different threads must have their own Yaml instance.
4. Loading a YAML Document
The library provides support for loading the document from a String or an InputStream. Majority of the code samples here would be based on parsing the InputStream.
Let’s start by defining a simple YAML document, and naming the file as customer.yaml:
firstName: "John"
lastName: "Doe"
age: 20
4.1. Basic Usage
Now we’ll parse the above YAML document with the Yaml class:
Yaml yaml = new Yaml();
InputStream inputStream = this.getClass()
.getClassLoader()
.getResourceAsStream("customer.yaml");
Map<String, Object> obj = yaml.load(inputStream);
System.out.println(obj);
The above code generates the following output:
{firstName=John, lastName=Doe, age=20}
By default, the load() method returns a Map instance. Querying the Map object each time would require us to know the property key names in advance, and it’s also not easy to traverse over nested properties.
4.2. Custom Type
The library also provides a way to load the document as a custom class. This option would allow easy traversal of data in memory.
Let’s define a Customer class and try to load the document again:
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. Implicit Types
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. Nested Objects and Collections
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. Type-Safe Collections
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. Loading Multiple Documents
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