Yaml Dateien mit C# parsen

Es gibt eine lange Liste von verbreiteten Markup-Formaten. Xml und Json sind dabei den allermeisten Entwicklern ein Begriff. Auch Yaml ist ein Format, welches nicht zuletzt durch den Einsatz bei Kubernetes oder anderen bekannten Applikationen eine größere Verbreitung genießt. Yaml wirkt auf den ersten Blick wie ein “Json ohne Klammern”. Das habe ich selbst tatsächlich auch lange so angenommen. Beschäftigt man sich etwas tiefer im Detail mit Yaml, so wird man aber schnell eines Besseren belehrt. Yaml bietet mehrere Features und hat etwa bei der Ermittlung der Datentypen ein mehr oder weniger komplexes Regelwerk. Aber warum überhaupt Yaml, wenn man doch auch Json einsetzen könnte? Yaml-Dateien könnten leichter durch einen Menschen geschrieben und gelesen werden. Aus diesem Grund wird es gerne für Konfigurationsdateien verwendet – so wie etwa bei Kubernetes. Somit kann es auch für C# Entwickler sinnvoll sein, anstelle von Json oder Xml eben Yaml für Konfigurationsdateien zu verwenden. Aus diesem Grund möchte ich mich in diesem Artikel mit dem Parsen von Yaml Dateien mit C# beschäftigen.

Struktur einer Yaml Datei

Bevor wir eine Yaml Datei mit C# parsen wollen, beschäftigen wir uns erst einmal mit dem Format. Ich habe eingangs geschrieben, dass man Yaml nach einem kurzen Blick fälschlicherweise als “Json ohne Klammern” sehen kann. Das liegt daran, dass es bei einfachen Beispielen i. d. R. auch so ist. Nachfolgende Codeausschnitte zeigen die gleiche Datei – einmal als Json und einmal als Yaml.

Yaml:

name: Dave
age: 35

Json:

{
  "Name": "Dave",
  "Age": 35
}

Das Beispiel ist sehr einfach gehalten. Wir beschreiben hier eine fiktive Person namens Dave. Dave ist 35 Jahre alt. An diesem Beispiel sehen wir, dass Yaml und Json sehr ähnlich aussieht. Im Yaml fehlen im Gegensatz zu Json im Wesentlichen die Klammern und die Anführungszeichen. Im nächsten Beispiel fügen wir noch die Stadt dazu, in der Dave lebt. Hier sehen wir bei der Eigenschaft City einen Unterschied. In Yaml haben wir eine Einrückung und je eine Zeile für Postleitzahl und Ort. Im Json werden Postleizahl und Ort in einem String zusammengefasst. Tatsächlich sind beide Werte identisch, da die Schreibweise in Yaml dafür sorgt, dass die aufgelisteten Text-Fragmente zu einem String Wert zusammengefügt werden.

Yaml:

name: Dave
age: 35
city:
  91052
  Erlangen

Json:

{
  "Name": "Dave",
  "Age": 35,
  "City": "91052 Erlangen"
}

Obiges Beispiel soll verdeutlichen, dass in Yaml mehr Regeln stecken, als man zunächst vermutet. Ich empfehle daher, sich mit den Details des Yaml-Formats zu beschäftigen, bevor man sich um das Parsing kümmert. Eine gute Quelle ist dabei die englische Wikipedia Seite zum Yaml-Format [1]. Es finden sich aber auch viele weitere Quellen im Internet wie etwa auf fileformat.com [2]. Im weiteren Verlauf dieses Artikels gehe ich somit nur noch auf Details zum Yaml-Format ein, wenn diese für das Parsing relevant sind.

Yaml Dateien mit YamlDotNet parsen

Ich verwende YamlDotNet für das Parsen von Yaml Dateien [3]. Die Beispiele für dieses Projekt habe ich auf GitHub in meinem HappyCoding Repository hochgeladen [4]. Den Stand des Quellcodes zum Zeitpunkt dieses Artikels habe ich als Download weiter unten im Artikel angehängt.

YamlDotNet verhält sich zunächst sehr ähnlich wie bekannte Json-Bibliotheken. Es wird ein Serializer und Deserializer zur Verfügung gestellt. In den Beispielen dieses Artikels beschäftigen wir uns mit dem Deserializer. Nachfolgender Codeausschnitt zeigt, wie ich den Deserializer in diesen Beispielen verwende. Bei der Namenskonvention setze ich auf CamelCase. Das führt dazu, dass etwa die C#-Eigenschaft “Name” in der Yaml Datei als “name” gelesen wird. Weiterhin stelle ich den Deserializer so ein, dass nicht abgebrochen wird, wenn zu einer Eigenschaft in der Yaml Datei keine passende Eigenschaft in der C# Klasse gefunden wurde. TModel ist die C# Klasse, in die deserialisiert wird. Nach dem Deserialisieren serialisiere ich das Objekt direkt wieder in Json. Im Beispielprogramm wird dieses Json schließlich in der Console ausgegeben. Anhand des ausgegebenen Json kann schließlich geprüft werden, ob alle Inhalte der Yaml Datei korrekt gelesen wurden.

var yamlDeserializer = new DeserializerBuilder()
    .WithNamingConvention(CamelCaseNamingConvention.Instance)
    .IgnoreUnmatchedProperties()
    .Build();

var deserializedModel = yamlDeserializer.Deserialize<TModel>(fullYamlInputString);

var jsonOutput = JsonSerializer.Serialize(deserializedModel, _jsonSerializerOptions)

Ein Objekt parsen

Kommen wir zum ersten Beispiel. Nachfolgend sehen wir eine ähnliche Yaml Datei wie weiter oben. Wir sehen hier also das Mapping eines einzelnen Objekts. YamlDotNet versucht beim Deserialisieren die Werte aus der Yaml Datei in die in der C# Klasse angegebenen Datentypen zu konvertieren. Bei dem Feld birthDate wird beispielsweise das an der C# Eigenschaft angegebene DateTime verwendet. Eine Besonderheit dieses Beispiels ist der String in der Eigenschaft city. In der Json Ausgabe auf der rechten Seite sehen wir aber, dass der Wert korrekt aus der Yaml Datei gelesen wurde.

Yaml (Input):

name: Dave
age: 35
birthDate: 1987-07-23 
city:
  91052 
  Erlangen

C# Klasse zur Yaml-Deserialisierung:

public class SimpleObjectModel
{
    public string Name { get; set; } = string.Empty;

    public int Age { get; set; }
    
    public DateTime BirthDate { get; set; }

    public string City { get; set; } = string.Empty;
}

Json (Output):

{
  "Name": "Dave",
  "Age": 35,
  "BirthDate": "1987-07-23T00:00:00",
  "City": "91052 Erlangen"
}

Ein Objekt mit Auflistungen parsen

Im nächsten Beispiel fügen wir zwei Auflistungen hinzu. Hierbei verwende ich in der Yaml Datei zwei verschiedene Schreibweisen. Einmal eine Liste von Strings in einer Zeile (Zeile 4). Einmal eine Liste von Objekten, bei der jedes Listenelement mit einen – startet (ab Zeile 6). Auf C#-Seite machen diese beiden Varianten keinen Unterschied, beide werden als Array eingelesen. Gleiches gilt für die Json Ausgabe.

Yaml (Input):

name: Dave
age: 35
birthDate: 1987-07-23
programmingLanguages: [CSharp, JavaScript, TypeScript]
additionalSkills:
  - name: Operating Systems
    description: Windows, Linux, maxOS
    level: good
  - name: Language
    description: German, English
    level: good

C# Klasse zur Yaml-Deserialisierung:

public class SimpleChildCollectionsModel
{
    public string Name { get; set; } = string.Empty;

    public int Age { get; set; }
    
    public DateTime BirthDate { get; set; }

    public string[] ProgrammingLanguages { get; set; } = Array.Empty<string>();

    public AdditionalSkill[] AdditionalSkills { get; set; } = Array.Empty<AdditionalSkill>();
}

public class AdditionalSkill
{
    public string Name { get; set; } = string.Empty;

    public string Description { get; set; } = string.Empty;

    public string Level { get; set; } = string.Empty;
}

Json (Output):

{
  "Name": "Dave",
  "Age": 35,
  "BirthDate": "1987-07-23T00:00:00",
  "ProgrammingLanguages": [
    "CSharp",
    "JavaScript",
    "TypeScript"
  ],
  "AdditionalSkills": [
    {
      "Name": "Operating Systems",
      "Description": "Windows, Linux, maxOS",
      "Level": "good"
    },
    {
      "Name": "Language",
      "Description": "German, English",
      "Level": "good"
    }
  ]
}

Parsen von mehrzeiligen Strings

Im nächsten Beispiel schauen wir uns an, wie mehrzeilige Strings gelesen werden. Yaml sieht hierbei mehrere Möglichkeiten vor. Im Feld multilineWithLineBreaks befindet sich ein String, bei der jeder Zeilenumbruch in der Yaml Datei als Zeilenumbruch eingelesen wird. Im Feld multilineWithoutLineBreaks dagegen werden die Zeilenumbrüche lediglich als Leerzeichen eingelesen. Für die C# Klasse sind beide Varianten Strings. In der Json Ausgabe ist gut zu erkennen, dass die Zeilenumbrüche wie gewünscht gelesen bzw. zu Leerzeichen konvertiert wurden.

Yaml (Input):

name: Dave
multilineWithLineBreaks: |
  This text is a multiline text
  with some linebreaks
  in it
multilineWithoutLineBreaks: >
  this text does not has
  a linebreak
  in it

C# Klasse zur Yaml-Deserialisierung:

public class ObjectWithMultilineStringsModel
{
    public string Name { get; set; } = string.Empty;

    public string MultilineWithLineBreaks { get; set; } = string.Empty;

    public string MultilineWithoutLineBreaks { get; set; } = string.Empty;
}

Json (Output):

{
  "Name": "Dave",
  "MultilineWithLineBreaks": "This text is a multiline text\nwith some linebreaks\nin it\n",
  "MultilineWithoutLineBreaks": "this text does not has a linebreak in it"
}

Parsen von dynamischen Inhalten per Dictionary

Im nächsten Beispiel schauen wir den Fall an, dass wir noch nicht genau wissen, welche Felder in der Yaml Datei verwendet werden. Im Yaml haben wir hier das Feld metadata eingefügt, welches ein Objekt mit einer Liste von Feldern enthält. Da wir nicht wissen, was hier enthalten ist, können wir auf C# Seite eine Dictionary<string, string> verwenden, in die alle Schlüssel-Wert Paare geschrieben werden sollen. An der Json Ausgabe sehen wir, dass alle Felder korrekt eingelesen wurden.

Yaml (Input):

containerName: my-nginx
imageName: nginx
metadata:
  application: dummyApplication
  group: myGroup
  groupId: 2
  owner: myOwner

C# Klasse zur Yaml-Deserialisierung:

public class ObjectWithDictionaryModel
{
    public string ContainerName { get; set; } = string.Empty;

    public string ImageName { get; set; } = string.Empty;
    
    public Dictionary<string, string>? Metadata { get; set; }
}

Json (Output):

{
  "ContainerName": "my-nginx",
  "ImageName": "nginx",
  "Metadata": {
    "application": "dummyApplication",
    "group": "myGroup",
    "groupId": "2",
    "owner": "myOwner"
  }
}

Parsen von Verlinkungen

Im letzten Beispiel schauen wir uns noch das Verhalten bei Verlinkungen (anchor and references) an. Yaml Dateien ermöglichen es, dass man ein und das selbe Objekt nur einmal schreiben muss, auch wenn es an mehreren Stellen des Dokuments verwendet wird. Json kann etwas vergleichbares im Standard nicht. In der Yaml Datei sehen wir, dass der Wert für das Feld city im Feld favoriteCity wiederverwendet wird. In der Json Ausgabe kommt entsprechend der gleiche Wert wie bei der Eigenschaft City an.

Yaml (Input):

name: Dave
age: 35
birthDate: 1987-07-23 
city: &MyCity
  91052 
  Erlangen
favoriteCity: *MyCity

C# Klasse zur Yaml-Deserialisierung:

public class YamlWithReferencesModel
{
    public string Name { get; set; } = string.Empty;

    public int Age { get; set; }
    
    public DateTime BirthDate { get; set; }

    public string City { get; set; } = string.Empty;

    public string FavoriteCity { get; set; } = string.Empty;
}

Json (Output):

{
  "Name": "Dave",
  "Age": 35,
  "BirthDate": "1987-07-23T00:00:00",
  "City": "91052 Erlangen",
  "FavoriteCity": "91052 Erlangen"
}

Fazit

Man könnte hier noch viele weitere Beispiele bringen. Ich denke aber, die Vorangegangenen geben einen guten Eindruck, wie das Parsen von Yaml Dateien funktioniert und welche Möglichkeiten man damit hat. Allgemein halte ich Yaml für ein sehr spannendes Format für Konfigurationsdateien und werde es vermutlich in Zukunft immer wieder verwenden. Zumindest bleibt es als sinnvolle Alternative zu Xml oder Json im Hinterkopf.

Downloads

  1. Quellcode der Beispiele aus diesem Artikel
    https://www.rolandk.de/files/2022/HappyCoding.YamlParsing.zip

Verweise

  1. Wikipedia-Eintrag zum Yaml-Format
    https://en.wikipedia.org/wiki/YAML
  2. Infos zum Yaml-Format auf fileformat.com
    https://docs.fileformat.com/programming/yaml/
  3. GitHub Repository von YamlDotNet
    https://github.com/aaubry/YamlDotNet
  4. Beispiel-Quellcode für diesen Artikel auf GitHub
    https://github.com/RolandKoenig/HappyCoding/tree/main/2022/HappyCoding.YamlParsing

Ebenfalls interessant

  1. Testautomatisierung beim Parsing von Dokumenten
    https://www.rolandk.de/wp-posts/2022/08/testautomatisierung-beim-parsing-von-dokumenten/

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.