Analisi di YAML con SnakeYAML

1. Panoramica

In questo tutorial, impareremo come usare la libreria SnakeYAML per serializzare oggetti Java in documenti YAML e viceversa .

2. Configurazione del progetto

Per utilizzare SnakeYAML nel nostro progetto, aggiungeremo la seguente dipendenza Maven (l'ultima versione può essere trovata qui):

 org.yaml snakeyaml 1.21 

3. Punto di ingresso

La classe Yaml è il punto di ingresso per l'API:

Yaml yaml = new Yaml();

Poiché l'implementazione non è thread-safe, diversi thread devono avere la propria istanza Yaml .

4. Caricamento di un documento YAML

La libreria fornisce il supporto per il caricamento del documento da una stringa o da un InputStream . La maggior parte degli esempi di codice qui sarebbe basata sull'analisi di InputStream .

Iniziamo definendo un semplice documento YAML e denominando il file come customer.yaml :

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

4.1. Utilizzo di base

Ora analizzeremo il documento YAML sopra con la classe Yaml :

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

Il codice precedente genera il seguente output:

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

Per impostazione predefinita, il metodo load () restituisce un'istanza Map . Interrogare ogni volta l'oggetto Map richiederebbe di conoscere in anticipo i nomi delle chiavi delle proprietà e inoltre non è facile attraversare proprietà annidate.

4.2. Tipo personalizzato

La libreria fornisce anche un modo per caricare il documento come una classe personalizzata . Questa opzione consentirebbe un facile attraversamento dei dati in memoria.

Definiamo una classe Customer e proviamo a caricare nuovamente il documento:

public class Customer { private String firstName; private String lastName; private int age; // getters and setters }

Supponendo che il documento YAML sia deserializzato come un tipo noto, possiamo specificare un tag globale esplicito nel documento.

Aggiorniamo il documento e archiviamolo in un nuovo file customer_with_type.yaml:

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

Nota la prima riga del documento, che contiene le informazioni sulla classe da utilizzare durante il caricamento.

Ora aggiorneremo il codice usato sopra e passeremo il nuovo nome del file come input:

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

Il metodo load () ora restituisce un'istanza di tipo Customer . Lo svantaggio di questo approccio è che il tipo deve essere esportato come libreria per poter essere utilizzato dove necessario .

Tuttavia, potremmo usare il tag locale esplicito per il quale non ci viene richiesto di esportare le librerie.

Un altro modo per caricare un tipo personalizzato consiste nell'usare la classe Constructor . In questo modo possiamo specificare il tipo di root per un documento YAML da analizzare. Creiamo un'istanza Constructor con il tipo Customer come tipo di root e passiamola all'istanza Yaml .

Ora, caricando il file customer.yaml, otterremo l' oggetto Customer :

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

4.3. Tipi impliciti

Nel caso in cui non sia stato definito alcun tipo per una determinata proprietà, la libreria converte automaticamente il valore in un tipo implicito .

Per esempio:

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

Testiamo questa conversione di tipo implicita utilizzando uno scenario di test:

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

4.4. Oggetti e raccolte nidificati

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 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)); 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 where each object is of type Map. 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)); 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. Dumping YAML Documents

The library also provides a method to dump a given Java object into a YAML document. The output could be a String or a specified file/stream.

5.1. Basic Usage

We'll start with a simple example of dumping an instance of Map to a YAML document (String):

@Test public void whenDumpMap_thenGenerateCorrectYAML() { Map data = new LinkedHashMap(); 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. Custom Java Objects

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

Questo articolo ha illustrato gli usi della libreria SnakeYAML per serializzare oggetti Java in YAML e viceversa.

Tutti gli esempi possono essere trovati nel progetto GitHub: questo è un progetto basato su Maven, quindi dovrebbe essere facile da importare ed eseguire così com'è.