YAML mit Java und SnakeYAML

YAML ("YAML Ain't Markup Language", mißverständlich auch "yet another markup language") ist eine Datenformatsprache, die in einer Vielzahl von Anwendungen eingesetzt wird, darunter die Konfiguration von Continuous Integration / Continuous Deployment (CI/CD) Pipelines, Docker- und Kubernetes-Konfigurationen sowie andere Softwareanwendungen. In diesem Artikel zeigen wir die Verwendung von SnakeYAML mit Java, einer leistungsstarken Bibliothek, die das Laden von YAML-Dateien als Map oder direkt in benutzerdefinierte Typen (POJOs) ermöglicht.

SnakeYAML ermöglicht das Laden von YAML-Daten sowohl aus einem InputStream als auch direkt aus einem String. Zusätzlich bietet die Bibliothek die Flexibilität, YAML-Daten entweder als Plain Old Java Object (POJO) bzw. benutzerdefinierte Klasse oder einfach als java.util.Map -Objekt zu laden.

Die Einbindung in ein Projekt ist ganz einfach über Maven oder Gradle (und natürlich auch direkt über den Classpath). Die aktuelle Version der Library findet man auf Maven Central oder direkt über die Projektseite in Bitbucket.

YAML als java.util.Map laden

Das Codebeispiel öffnet einen InputStream, um YAML-Daten aus einer Datei zu lesen. Anschließend wird die SnakeYAML-Bibliothek verwendet, um den Inhalt des InputStreams zu parsen und in eine Map mit Schlüssel-Wert-Paaren zu laden.

File yamlFile = loadYamlFile();
try (InputStream inputStream = new FileInputStream(yamlFile)){
    Yaml yaml = new Yaml();
    Map<String, Object> fooBar = yaml.load(inputStream);
} catch (Exception e) {
    // Exception Handling
}

YAML als POJO laden

Dieses Beispiel lädt eine YAML-Datei über einen InputStream aus einer Datei und konvertiert sie anschließend in ein Objekt vom Typ "FooBar":

File yamlFile = loadYamlFile();
try (InputStream inputStream = new FileInputStream(yamlFile)){
    Yaml yaml = new Yaml();
    return yaml.loadAs(inputStream, FooBar.class);
} catch (Exception e) {
    // Exception Handling
}

"FooBar" ist dabei ein gewöhnliches POJO bzw. benutzerdefinierte Klasse und kann beliebige Attribute und natürlich auch andere benutzerdefinierte Typen beinhalten. Zur Vereinfachung generiert Lombok für uns alle Getter und Setter:

@Getter @Setter
public class FooBar {
    int nr;
    String name;
    List<Baz> baz;
}

@Getter @Setter
public class Baz {
    String name;
}

Ein korrespondierendes YAML sieht so aus:

nr: 123
name: "jberries"
baz:
    - name: "baz1"
    - name: "baz2"
    - name: "baz3"

Der Vollständigkeit halber ein Unit Test für diesen Fall:

@Test
public void testYamlMapping() {
    String yamlString = "nr: 123\ndate: ..."; // siehe oberes Beispiel
    Yaml yaml = new Yaml();
    yaml.loadAs(yamlString, FooBar.class);

    assertEquals(123, fooBar.getNr());
    assertEquals("jberries", fooBar.getName());

    List<Baz> bazList = fooBar.getBaz();
    assertEquals(3, bazList.size());
    assertEquals("baz1", bazList.get(0).getName());
    assertEquals("baz2", bazList.get(1).getName());
    assertEquals("baz3", bazList.get(2).getName());
}

Fehlende Attribute - "Unable to find property"

Wenn in der YAML-Datei Eigenschaften auftauchen, die nicht auf eine POJO-Eigenschaft abgebildet werden können, wird eine Exception mit der Meldung "Unable to find property" ausgelöst.

Um keine Exceptions in einem solchen Fall zu erhalten, bietet die Methode setSkipMissingProperties in PropertiesUtil eine Lösung. Allerdings erfordert dies, dass sowohl der Constructor, der sich um die Instanziierung benutzerdefinierter Java-Klassen kümmert, als auch der Representer, der Details der YAML-Struktur definiert, manuell instanziiert werden müssen.

Representer representer = new Representer(new DumperOptions());
representer.getPropertyUtils().setSkipMissingProperties(true);

Yaml yaml = new Yaml(new Constructor(new LoaderOptions()), representer);

Generische Typen

Wir können das obere Beispiel erweitern und die Klasse FooBar um eine Liste mit generischen Typen ergänzen:

@Data
public class<T> FooBarGeneric {
    int nr;
    List<T> baz;
}

In einem solchen Fall würde SnakeYAML nicht in der Lage sein, den Typen der Elemente in der Liste zu erkennen.

nr: 123
baz:
    - "baz1"
    - "baz2"
    - "baz3"

Über die Klasse TypeDescription können wir hier den generischen Typen aufschlüsseln. In dem einfachen Fall geben wir an, dass "baz" eine Liste von String-s definiert.

TypeDescription bazDescription = new TypeDescription(FooBarGeneric.class);
bazDescription.addPropertyParameters("baz", String.class);

Constructor constructor = new Constructor(new LoaderOptions());
constructor.addTypeDescription(bazDescription);

Yaml yaml = new Yaml(constructor);
@SuppressWarnings("unchecked")
FooBarGeneric<String> fooBar = yaml.loadAs(yamlString, FooBarGeneric.class);

YAML aus Java Generieren

Durch die Verwendung der Methode yaml.dump ist es einfach, ein Java-POJO oder eine Map in einen String zu konvertieren. Optional können wir auch einen eigenen java.io.Writer (zum Beispiel StringWriter) als Parameter angeben.

Yaml yaml = new Yaml();

StringWriter writer = new StringWriter();
yaml.dump(map_oder_pojo, writer);
String yamlString = writer.toString();

// oder direkt zum String
String yamlString = yaml.dump(map_oder_pojo);