YAML-zu-XML-Konverter

Wandelt YAML nach XML, komplett in Ihrem Browser.

Eingabe
Ausgabe
WartetFügen Sie ein Dokument ein. Die Prüfung läuft während der Eingabe.

Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.

Fügen Sie oben YAML ein, und daneben erscheint eingerücktes, wohlgeformtes XML, mit Deklaration und jedem Sonderzeichen maskiert. Parst das YAML nicht, bekommen Sie die erste Beschwerdezeile des Parsers selbst statt eines leeren Bereichs, und musste ein Schlüssel umbenannt werden, um ein zulässiger XML-Elementname zu sein, listet das Werkzeug auf, was sich geändert hat.

Das ist die Richtung, die Sie brauchen, wenn etwas Altes etwas Neues lesen muss: eine Integration, die nur XML annimmt, ein SOAP-Endpunkt, ein per XSD validiertes Austauschformat, eine Fixture für einen in YAML konfigurierten Dienst. Es hat überraschend viele scharfe Kanten, und die meisten kommen von YAML und nicht von XML.

Das Parsen erledigt js-yaml, geladen nur beim Öffnen dieser Seite und nicht auf jeder Seite der Website. Alles läuft in diesem Tab, und nichts wird hochgeladen, was zählt, weil in YAML die Konfiguration wohnt und in der Konfiguration die Zugangsdaten.

Wie sich die drei YAML-Knotenarten abbilden

YAML hat genau drei Knotenarten, und jede hat ein XML-Gegenstück. Eine Abbildung wird zu einer Menge von Kindelementen, eines je Schlüssel, mit dem Schlüssel als Elementnamen. Eine Sequenz wiederholt den Namen ihres Elternelements einmal je Mitglied, ohne Hülle, denn Wiederholung ist die Art, wie XML eine Liste ausdrückt. Ein Skalar wird zum Textinhalt seines Elements.

Darüber sitzt das Wurzelproblem. YAML erlaubt jeden Knoten an der Spitze eines Dokuments; XML verlangt genau eine Wurzel. Eine Abbildung mit einem Schlüssel hat bereits eine natürliche Wurzel, dieser Schlüssel wird also das Wurzelelement. Eine Abbildung mit zwei oder mehr Schlüsseln, eine Sequenz auf oberster Ebene oder ein nackter Skalar werden in ein einzelnes Element gewickelt, standardmäßig root genannt und in der Steuerzeile änderbar.

Zwei Folgen der Sequenzregel sind es wert, gekannt zu werden. Eine leere Sequenz erzeugt überhaupt nichts, der Schlüssel verschwindet also: null Wiederholungen eines Elements sind null Elemente. Und eine Sequenz, die direkt in einer anderen Sequenz steckt, flacht ab, denn die innere hat keinen eigenen Namen, den sie verwenden könnte.

order:
  id: '00042'
  line:
    - Widget
    - Gasket
  note: null
  tags: []

<?xml version="1.0" encoding="UTF-8"?>
<order>
  <id>00042</id>
  <line>Widget</line>
  <line>Gasket</line>
  <note/>
</order>
Eine Abbildung, eine Sequenz, ein Null und eine leere Sequenz.

YAML hat Ihre Typen entschieden, bevor XML sie sieht

Das ist das Wichtigste auf dieser Seite, und es ist keine Eigenschaft dieses Konverters. YAML löst einen einfachen Skalar anhand seiner Schreibung zu einem Typ auf, innerhalb des Parsers. Wenn ein Wert den XML-Schreiber erreicht, ist er bereits eine Zahl, ein Boolean, ein Datum oder eine Zeichenkette, und XML hat kein Typsystem, mit dem sich der Unterschied zurückholen ließe.

js-yaml implementiert das Kernschema von YAML 1.2 plus den Timestamp-Typ, was Folgendes ergibt. Sie können jede Zeile davon prüfen, indem Sie den Wert einfügen:

  • true und false sind Booleans und werden als Text true und false geschrieben. yes und no bleiben hier Zeichenketten, aber ein YAML-1.1-Parser wie PyYAML oder Ansible liest no als false, dieselbe Datei ergibt mit anderem Werkzeug also anderes XML.
  • Führende Nullen sind vor der Umwandlung weg: 01730 löst zur Zahl 1730 auf, und der XML-Schreiber kann nichts dagegen tun. Schreiben Sie '01730'.
  • Auch andere Zahlenbasen werden aufgelöst, 0x1F wird also 31 und wird als <hex>31</hex> geschrieben. Hexadezimale Farbcodes und Hardwarekennungen brauchen Anführungszeichen.
  • Eine YAML-Ganzzahl wird im Browser ein Double, eine neunzehnstellige Kennung hat ihre unteren Stellen also schon verloren, bevor der Schreiber beteiligt ist. Setzen Sie Kennungen immer in Anführungszeichen.
  • Datumsangaben lösen zu Zeitstempeln auf, und ein Zeitstempel hat keine Textdarstellung, die der Schreiber erzeugen könnte, 2024-01-05 kommt also als leeres <when/> heraus. Setzen Sie es in Anführungszeichen, und es wird als Text geschrieben.

YAML-Schlüssel sind häufig keine zulässigen XML-Namen

XML 1.0 Abschnitt 2.3 sagt, ein Elementname beginnt mit einem Buchstaben, einem Unterstrich oder einem Doppelpunkt und setzt sich mit diesen plus Ziffern, Bindestrichen und Punkten fort. Ein YAML-Schlüssel hat keine solche Einschränkung: „2024 total“, „user@email“ und die leere Zeichenkette sind alle gewöhnliche Schlüssel, und keiner kann ein Elementname sein.

Jeder wird umbenannt statt zurückgewiesen, und jede Umbenennung wird gemeldet. Unzulässige Zeichen werden eins zu eins durch einen Unterstrich ersetzt, und ein Name, der immer noch mit einer Ziffer beginnt, bekommt einen Unterstrich davor. Ersetzen statt Streichen ist Absicht: Streichen würde „2024 total“ und „2024total“ zum selben Element machen und zwei verschiedene Felder verschmelzen. Also wird „2024 total“ zu _2024_total, „2024-total“ zu _2024-total, weil ein Bindestrich schon zulässig ist, und „user@email“ zu user_email. Dicht ist das nicht: „first name“ und „first_name“ landen beide auf first_name, benennen Sie also Schlüssel um, die sich nur in der Zeichensetzung unterscheiden.

YAML erlaubt auch Schlüssel, die keine Zeichenketten sind: 2024 ist eine Ganzzahl, true ein Boolean, und die Syntax für explizite Schlüssel erlaubt eine ganze Sequenz als Schlüssel. Alle werden in Zeichenketten verwandelt, bevor sie Elementnamen werden. Eine Eigenheit: Schlüssel, die wie Array-Indizes aussehen, werden zuerst und in aufsteigender numerischer Reihenfolge aufgezählt, eine Abbildung, die 2, 10 und name mischt, gibt ihre Elemente also nicht in der Reihenfolge aus, in der Sie sie geschrieben haben.

Ströme mit mehreren Dokumenten, Anker und Merge-Schlüssel

Ein YAML-Strom kann mehrere Dokumente enthalten, getrennt durch drei Bindestriche, und Kubernetes-Manifeste tun das routinemäßig. XML hat genau eine Wurzel, also werden alle gelesen und gewickelt: ein einzelnes <documents>-Element mit einem <document>-Kind je YAML-Dokument, dazu ein Hinweis, wie viele gefunden wurden. Die meisten Konverter kürzen still auf das erste, was Sie in der Produktion entdecken, wenn zwei Drittel eines Manifests leise verschwinden.

Anker, Aliasse und Merge-Schlüssel löst der Parser auf, und sie sind verschwunden, wenn das XML geschrieben wird. Sie bekommen das vollständig expandierte Ergebnis, das korrekt ist und erheblich größer als die Eingabe sein kann: Ein Basisblock, der in vierzig Dienste hineingealiast wird, ergibt vierzig Kopien. Diese Expansion passiert im Speicher dieses Tabs, starkes Aliasing kann also langsam sein, und die Größenobergrenze gilt für die Ausgabe ebenso wie für die Quelle.

Kommentare bleiben nicht erhalten, denn sie sind nicht Teil des YAML-Datenmodells, und der Parser reicht sie nie weiter. Ein leerer Strom oder einer, der nur Kommentare enthält, wird als leer gemeldet statt in ein leeres Wurzelelement umgewandelt.

Das im Code tun

In jeder Sprache zwei Schritte: das YAML mit einem sicheren Loader laden, dann XML mit etwas schreiben, das richtig maskiert. Das Sicherheitsflag sitzt hier auf der YAML- und nicht auf der XML-Seite. Mehrere YAML-Bibliotheken instanziieren standardmäßig oder über ein einziges Tag im Dokument beliebige Klassen aus der Datei, und das ist Remote Code Execution im Kostüm einer Konfiguration.

import yaml from 'js-yaml';
import { XMLBuilder } from 'fast-xml-parser';

// load() uses the default schema, which constructs no JavaScript types.
// Do not swap in js-yaml's extended schema for untrusted input.
const docs = [];
yaml.loadAll(yamlSource, (d) => docs.push(d));

if (docs.length === 0) throw new Error('The YAML document is empty.');
// XML has one root; a multi-document stream needs wrapping, not truncating.
let data = docs.length > 1 ? { documents: { document: docs } } : docs[0];

if (data === null || typeof data !== 'object' || Array.isArray(data)
    || Object.keys(data).length !== 1) {
  data = { root: data };
}

const builder = new XMLBuilder({
  ignoreAttributes: false,
  attributeNamePrefix: '@_',
  textNodeName: '#text',
  format: true,
  indentBy: '  ',
  suppressEmptyNode: true,
});

console.log('<?xml version="1.0" encoding="UTF-8"?>');
console.log(builder.build(data));

// XMLBuilder does not sanitise names. A YAML key of "2024 total" is written
// verbatim and the result will not parse, so validate before you ship it.
import re
import yaml
import xmltodict


def legal_name(key):
    """Replace illegal characters rather than stripping them, so distinct
    keys stay distinct. Prefix a leading digit."""
    name = re.sub(r'[^\w.\-:]', '_', str(key), flags=re.UNICODE)
    return name if re.match(r'^[A-Za-z_:]', name) else '_' + name


def sanitise(node):
    if isinstance(node, dict):
        return {legal_name(k): sanitise(v) for k, v in node.items()}
    if isinstance(node, list):
        return [sanitise(v) for v in node]
    if isinstance(node, bool):
        return 'true' if node else 'false'
    return node if node is None else str(node)


# safe_load, never load: yaml.load with the default Loader will construct
# arbitrary Python objects from !!python tags in the document.
docs = [d for d in yaml.safe_load_all(yaml_source) if d is not None]
if not docs:
    raise SystemExit('The YAML document is empty.')

data = {'documents': {'document': docs}} if len(docs) > 1 else docs[0]
if not isinstance(data, dict) or len(data) != 1:
    data = {'root': data}

print(xmltodict.unparse(sanitise(data), pretty=True, indent='  ',
                        full_document=True))

# PyYAML applies the YAML 1.1 resolver, so an unquoted no is False here and
# a string in js-yaml. Quote anything whose type you care about.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;

// Jackson's YAML module wraps SnakeYAML but binds only to JsonNode and to
// classes you name, so the SnakeYAML deserialisation gadget problem
// (CVE-2022-1471, the default Constructor instantiating arbitrary types)
// is not reachable through this API. Using SnakeYAML directly, construct it
// as: new Yaml(new SafeConstructor(new LoaderOptions()))
JsonNode tree = new YAMLMapper().readTree(yamlSource);

XmlMapper xml = new XmlMapper();
xml.enable(SerializationFeature.INDENT_OUTPUT);

String out = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    + xml.writer().withRootName("root").writeValueAsString(tree);

// readTree reads the first document only. For a multi-document stream use
// new YAMLMapper().readerFor(JsonNode.class).readValues(yamlSource)
// and wrap the results yourself.
using Newtonsoft.Json;
using YamlDotNet.Serialization;

// YamlDotNet's Deserializer binds only to types you name and does not
// resolve arbitrary .NET types from tags in the document.
var yaml = new DeserializerBuilder().Build();
object? tree = yaml.Deserialize<object>(new StringReader(yamlSource));

if (tree is null) throw new InvalidOperationException("The YAML is empty.");

// Round-trip through JSON so Json.NET can do the XML writing, including the
// escaping. The second argument names the root, which YAML does not supply
// and XML requires.
string json = JsonConvert.SerializeObject(tree);
var document = JsonConvert.DeserializeXmlNode(json, "root")
    ?? throw new InvalidOperationException("Nothing to write.");

var settings = new System.Xml.XmlWriterSettings { Indent = true, IndentChars = "  " };
using var writer = System.Xml.XmlWriter.Create(Console.Out, settings);
document.Save(writer);

// Json.NET will not sanitise names: a YAML key of "2024 total" throws
// XmlException when the node is created. Rewrite keys before this point.
<?php
use Symfony\Component\Yaml\Yaml;

// Symfony's parser never instantiates PHP objects unless you pass
// PARSE_OBJECT or PARSE_OBJECT_FOR_MAP. Do not pass either for input you did
// not write. The ext-yaml alternative, yaml_parse(), is governed by the
// yaml.decode_php ini setting, which is off by default; check it.
$data = Yaml::parse($source, Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE);

function legal_name(string $key): string {
    $name = preg_replace('/[^\w.\-:]/u', '_', $key);
    return preg_match('/^[A-Za-z_:]/', $name) ? $name : '_' . $name;
}

function write_node(XMLWriter $w, string $name, mixed $value): void {
    if (is_array($value) && array_is_list($value)) {
        foreach ($value as $v) write_node($w, $name, $v);   // repeat, no wrapper
        return;
    }
    $w->startElement(legal_name($name));
    if (is_array($value)) {
        foreach ($value as $k => $v) write_node($w, (string) $k, $v);
    } elseif (is_bool($value)) {
        $w->text($value ? 'true' : 'false');
    } elseif ($value !== null) {
        $w->text((string) $value);
    }
    $w->endElement();
}

$single = count($data) === 1;
$w = new XMLWriter();
$w->openMemory();
$w->setIndent(true);
$w->setIndentString('  ');
$w->startDocument('1.0', 'UTF-8');
write_node($w, $single ? (string) array_key_first($data) : 'root',
               $single ? reset($data) : $data);
$w->endDocument();
echo $w->outputMemory();
# yq v4 (Mike Farah) converts directly.
yq -p=yaml -o=xml '.' config.yaml

# yq writes no XML declaration and no wrapper, so a multi-key document
# produces several roots. Wrap it first:
yq -p=yaml -o=xml '{"root": .}' config.yaml

# A multi-document stream needs collecting into one root explicitly, or yq
# emits one XML fragment per document:
yq ea -p=yaml -o=xml '{"documents": {"document": [.]}}' manifests.yaml

# Always check the result. yq does not sanitise element names, so a key with
# a space in it produces XML that will not parse:
yq -p=yaml -o=xml '{"root": .}' config.yaml | xmllint --noout --nonet -

Was Sie in jedem dieser Fälle richtig einstellen müssen, ist der Loader, nicht der Schreiber. yaml.load in Python, der Standard-Constructor von SnakeYAML in Java und yaml_parse mit aktiviertem yaml.decode_php bauen alle beliebige Objekte aus Tags im Dokument. Eine YAML-Datei ist Daten, bis Sie einen Loader benutzen, der sie etwas anderes sein lässt.

Häufige Fragen

Wird mein YAML irgendwohin hochgeladen?

Nein. YAML-Parser und XML-Schreiber sind beide JavaScript, das in diesem Tab läuft, und es gibt keine serverseitige Komponente, die sie erreichen könnten. Öffnen Sie den Netzwerk-Tab Ihrer Entwicklerwerkzeuge, fügen Sie ein Dokument ein, und Sie sehen die eigenen Assets der Seite einmal laden und danach nichts.

Hier lohnt sich die Prüfung besonders. In YAML wohnt die Konfiguration: Kubernetes-Secrets, Variablen von CI-Pipelines, Ansible-Inventare mit Hostnamen und Benutzernamen, Compose-Dateien mit Datenbankpasswörtern darin.

Warum hat sich meine Postleitzahl, Versionsnummer oder ID verändert?

Weil YAML sie verändert hat, nicht der XML-Schreiber. YAML leitet den Typ eines Skalars aus seiner Schreibung ab, also ist 01730 die Zahl 1730, 1.10 der Float 1.1, und eine neunzehnstellige Kennung passt nicht in ein Double. Das alles geschieht im YAML-Parser, bevor irgendetwas XML-Bezogenes läuft.

Die Korrektur liegt im YAML: Setzen Sie den Wert in Anführungszeichen. '01730', '1.10' und '9007199254740993' kommen alle als Zeichenketten an und werden genau so geschrieben, wie getippt. Hat ein Generator das YAML erzeugt, hätte er sie quoten müssen.

Was passiert mit einer YAML-Datei, die mehrere durch --- getrennte Dokumente enthält?

Alle werden gelesen und gewickelt. Sie bekommen ein einzelnes <documents>-Element mit einem <document>-Kind je YAML-Dokument, und das Hinweisfeld sagt, wie viele gefunden wurden.

Die Alternative, die die meisten Konverter wählen, ist, das erste Dokument umzuwandeln und den Rest still zu ignorieren. Das ist eine schlechte Voreinstellung für Kubernetes-Manifeste, wo eine Datei routinemäßig ein Deployment, einen Service und eine ConfigMap enthält, und zwei von dreien zu verlieren bemerkt man erst, wenn ein Rollout scheitert.

Wie werden Anker, Aliasse und Merge-Schlüssel behandelt?

Der Parser löst sie auf, und in der Ausgabe sind sie vollständig expandiert. Ein Anker markiert einen Knoten, ein Alias verweist darauf zurück, und ein Merge-Schlüssel wendet eine Abbildung in einer anderen an. Keines der drei existiert in XML, und keines überlebt.

Was Sie bekommen, ist korrekt, kann aber viel größer sein als die Eingabe: Ein Basisblock, der in vierzig Dienste hineingealiast wird, ergibt vierzig vollständige Kopien. Genau das meinte das YAML, nur dass YAML Sie es einmal schreiben ließ. Die Expansion passiert im Speicher dieses Tabs, starkes Aliasing kann also langsam sein.

Kann ich Werte als XML-Attribute statt als Kindelemente bekommen?

Ja. Stellen Sie dem Schlüssel in Ihrem YAML das Attributpräfix voran, das in der Steuerzeile steht und standardmäßig @_ lautet: Ein Schlüssel '@_id' mit dem Wert 7 wird dann zu einem id-Attribut am umgebenden Element statt zu einem <id>-Kind.

Sie müssen diesen Schlüssel quoten. Ein einfacher Skalar darf nicht mit @ beginnen, das YAML reserviert, ein ungequotetes @_id ist also ein Parsefehler, und die Meldung beschwert sich über die Einrückung statt über das Zeichen. Setzen Sie dort nur Skalare hin: Ein Attributwert kann keine Struktur enthalten, eine Abbildung unter einem @_-Schlüssel ergibt also einen zu Text verwandelten Brei statt verschachteltem XML.

Ist das erzeugte XML gültig?

Es ist wohlgeformt, was eine andere und schwächere Aussage ist. Jedes Element ist geschlossen, es gibt genau eine Wurzel, das Kaufmanns-Und, das Kleiner-als-Zeichen und die Folge ]]> sind im Text maskiert, Attributwerte maskieren zusätzlich das doppelte Anführungszeichen und die Leerraumzeichen, und oben steht eine UTF-8-Deklaration.

Gültigkeit heißt, zu einem Schema zu passen, und YAML trägt keines mit sich, aus dem sich eines ableiten ließe. Geht das XML irgendwohin, wo validiert wird, nehmen Sie es mit dem Schema, das dieses System veröffentlicht, in den XSD-Validator.

Verwandte Werkzeuge

Zum Weiterlesen