Convertisseur YAML vers XML

Convertit YAML en XML, entièrement dans votre navigateur.

Entrée
Sortie
En attenteCollez un document pour le vérifier. La validation se fait à la frappe.

Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.

Collez du YAML ci-dessus et un XML indenté et bien formé apparaît à côté, avec une déclaration et chaque caractère spécial échappé. Si le YAML ne s’analyse pas, vous obtenez la première ligne de plainte de l’analyseur lui-même plutôt qu’un volet vide, et s’il a fallu renommer une clé pour en faire un nom d’élément XML licite, l’outil liste ce qui a changé.

C’est le sens dont vous avez besoin quand quelque chose d’ancien doit lire quelque chose de récent : une intégration qui n’accepte que du XML, un point d’accès SOAP, un format d’échange validé par XSD, une fixture pour un service configuré en YAML. Les angles vifs y sont étonnamment nombreux, et la plupart viennent de YAML et non de XML.

L’analyse est faite par js-yaml, chargé seulement quand vous ouvrez cette page et non sur chaque page du site. Tout tourne dans cet onglet et rien n’est téléversé, ce qui compte parce que YAML est là où vit la configuration et que la configuration est là où vivent les identifiants.

Comment se projettent les trois sortes de nœuds YAML

YAML a exactement trois sortes de nœuds et chacune a une contrepartie XML. Une table devient un ensemble d’éléments enfants, un par clé, la clé servant de nom d’élément. Une séquence répète le nom de son élément parent une fois par membre, sans enveloppe, car la répétition est la façon dont XML exprime une liste. Un scalaire devient le contenu textuel de son élément.

Par-dessus se pose le problème de la racine. YAML permet n’importe quel nœud au sommet d’un document ; XML exige exactement une racine. Une table à une seule clé a déjà une racine naturelle : cette clé devient l’élément racine. Une table à deux clés ou plus, une séquence de premier niveau ou un scalaire nu sont enveloppés dans un seul élément, nommé root par défaut et modifiable dans la rangée de réglages.

Deux conséquences de la règle des séquences valent d’être connues. Une séquence vide ne produit rien du tout : la clé disparaît, car zéro répétition d’un élément fait zéro élément. Et une séquence imbriquée directement dans une autre séquence s’aplatit, car l’intérieure n’a pas de nom propre à utiliser.

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>
Une table, une séquence, un nul et une séquence vide.

YAML a décidé de vos types avant que XML ne les voie

C’est le point le plus important de la page, et ce n’est pas une propriété de ce convertisseur. YAML résout un scalaire nu vers un type d’après son orthographe, à l’intérieur de l’analyseur. Quand une valeur atteint l’écrivain XML, elle est déjà un nombre, un booléen, une date ou une chaîne, et XML n’a aucun système de types avec lequel retrouver la distinction.

js-yaml met en œuvre le schéma central de YAML 1.2 plus le type timestamp, ce qui donne ceci. Vous pouvez vérifier chaque ligne en collant la valeur :

  • true et false sont des booléens et s’écrivent comme les textes true et false. yes et no restent ici des chaînes, mais un analyseur YAML 1.1 comme PyYAML ou Ansible lit no comme false : le même fichier converti avec un outillage différent produit donc un XML différent.
  • Les zéros de tête ont disparu avant la conversion : 01730 se résout au nombre 1730, et l’écrivain XML n’y peut rien. Écrivez '01730'.
  • Les autres bases sont résolues aussi, donc 0x1F devient 31 et s’écrit <hex>31</hex>. Les codes couleur hexadécimaux et les identifiants matériels ont besoin de guillemets.
  • Un entier YAML devient un double dans le navigateur, donc un identifiant à dix-neuf chiffres a déjà perdu ses chiffres de poids faible avant que l’écrivain n’intervienne. Citez les identifiants, toujours.
  • Les dates se résolvent en horodatages, et un horodatage n’a pas de représentation textuelle que l’écrivain puisse produire : 2024-01-05 ressort donc en un <when/> vide. Citez-la et elle est écrite comme du texte.

Les clés YAML ne sont souvent pas des noms XML licites

La section 2.3 de XML 1.0 dit qu’un nom d’élément commence par une lettre, un tiret bas ou deux-points et continue par ceux-ci plus des chiffres, des traits d’union et des points. Une clé YAML n’a pas cette restriction : « 2024 total », « user@email » et la chaîne vide sont toutes des clés ordinaires et aucune ne peut être un nom d’élément.

Chacune est renommée plutôt que refusée, et chaque renommage est signalé. Les caractères illicites sont remplacés un pour un par un tiret bas, et un nom commençant encore par un chiffre reçoit un tiret bas devant. Remplacer plutôt que supprimer est délibéré : supprimer ferait de « 2024 total » et « 2024total » le même élément et fusionnerait deux champs distincts. Donc « 2024 total » devient _2024_total, « 2024-total » devient _2024-total car le trait d’union est déjà licite, et « user@email » devient user_email. Ce n’est pas étanche : « first name » et « first_name » aboutissent tous deux à first_name, alors renommez les clés qui ne diffèrent que par la ponctuation.

YAML autorise aussi des clés qui ne sont pas des chaînes : 2024 est un entier, true un booléen, et la syntaxe de clé explicite permet une séquence entière comme clé. Toutes sont converties en chaînes avant de devenir des noms d’éléments. Une bizarrerie : les clés qui ressemblent à des indices de tableau sont énumérées d’abord et par ordre numérique croissant, donc une table mêlant 2, 10 et name n’émettra pas ses éléments dans l’ordre où vous les avez écrits.

Flux à plusieurs documents, ancres et clés de fusion

Un flux YAML peut contenir plusieurs documents séparés par trois traits d’union, et les manifestes Kubernetes le font couramment. XML n’a qu’une racine, donc tous sont lus et enveloppés : un seul élément <documents> avec un enfant <document> par document YAML, et une note indiquant combien ont été trouvés. La plupart des convertisseurs tronquent silencieusement au premier, ce que vous découvrez en production quand les deux tiers d’un manifeste disparaissent sans bruit.

Les ancres, les alias et les clés de fusion sont résolus par l’analyseur et ont disparu au moment où le XML est écrit. Ce que vous obtenez est le résultat pleinement développé, qui est correct et peut être bien plus gros que l’entrée : un bloc de base aliasé dans quarante services produit quarante copies. Ce développement a lieu dans la mémoire de cet onglet, donc un aliasage lourd peut être lent, et le plafond de taille s’applique à la sortie autant qu’à la source.

Les commentaires ne sont pas préservés, car ils ne font pas partie du modèle de données YAML et l’analyseur ne les transmet jamais. Un flux vide, ou qui ne contient que des commentaires, est signalé comme vide plutôt que converti en un élément racine vide.

Le faire en code

Deux étapes dans chaque langage : charger le YAML avec un chargeur sûr, puis écrire du XML avec quelque chose qui échappe correctement. Ici le drapeau de sécurité est du côté YAML, pas du côté XML. Plusieurs bibliothèques YAML, par défaut ou via une seule balise dans le document, instancieront des classes arbitraires depuis le fichier, ce qui est de l’exécution de code à distance déguisée en configuration.

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 -

Le réglage à ne pas rater dans chacun de ces cas est le chargeur, pas l’écrivain. yaml.load en Python, le Constructor par défaut de SnakeYAML en Java et yaml_parse avec yaml.decode_php activé construiront tous des objets arbitraires à partir de balises du document. Un fichier YAML est une donnée jusqu’à ce que vous utilisiez un chargeur qui lui permette d’être autre chose.

Questions fréquentes

Mon YAML est-il téléversé quelque part ?

Non. L’analyseur YAML et l’écrivain XML sont tous deux du JavaScript exécuté dans cet onglet, et il n’y a aucun composant côté serveur qu’ils puissent atteindre. Ouvrez l’onglet Réseau de vos outils de développement, collez un document, et vous verrez les ressources de la page se charger une fois, puis plus rien.

Cela vaut d’être vérifié ici en particulier. YAML est là où vit la configuration : secrets Kubernetes, variables de pipeline de CI, inventaires Ansible avec noms d’hôtes et d’utilisateurs, fichiers compose contenant des mots de passe de base de données.

Pourquoi mon code postal, mon numéro de version ou mon identifiant a-t-il changé ?

Parce que c’est YAML qui l’a changé, pas l’écrivain XML. YAML déduit le type d’un scalaire de son orthographe : 01730 est le nombre 1730, 1.10 est le flottant 1.1, et un identifiant à dix-neuf chiffres ne tient pas dans un double. Tout cela se passe à l’intérieur de l’analyseur YAML, avant que quoi que ce soit de lié au XML ne s’exécute.

La correction est dans le YAML : citez la valeur. '01730', '1.10' et '9007199254740993' arrivent tous comme des chaînes et sont écrits exactement comme tapés. Si un générateur a produit le YAML, c’est lui qui aurait dû les citer.

Qu’advient-il d’un fichier YAML contenant plusieurs documents séparés par --- ?

Tous sont lus et enveloppés. Vous obtenez un seul élément <documents> avec un enfant <document> par document YAML, et le panneau de notes indique combien ont été trouvés.

L’alternative, que choisissent la plupart des convertisseurs, est de convertir le premier document et d’ignorer le reste en silence. C’est un mauvais réglage par défaut pour les manifestes Kubernetes, où un fichier contient couramment un Deployment, un Service et un ConfigMap, et perdre deux des trois n’est pas quelque chose qu’on remarque avant qu’un déploiement échoue.

Comment sont traités les ancres, les alias et les clés de fusion ?

Ils sont résolus par l’analyseur puis pleinement développés dans la sortie. Une ancre marque un nœud, un alias y renvoie, et une clé de fusion applique une table dans une autre. Aucun des trois n’existe en XML et aucun ne survit.

Ce que vous obtenez est correct mais peut être bien plus gros que l’entrée : un bloc de base aliasé dans quarante services produit quarante copies complètes. C’est ce que le YAML voulait dire, sauf que YAML vous a laissé l’écrire une seule fois. Le développement a lieu dans la mémoire de cet onglet, donc un aliasage lourd peut être lent.

Puis-je obtenir des valeurs comme attributs XML plutôt que comme éléments enfants ?

Oui. Préfixez la clé dans votre YAML avec le préfixe d’attributs affiché dans la rangée de réglages, qui est @_ par défaut : une clé '@_id' de valeur 7 devient alors un attribut id sur l’élément englobant plutôt qu’un enfant <id>.

Il faut citer cette clé. Un scalaire nu ne peut pas commencer par @, que YAML réserve, donc un @_id non cité est une erreur d’analyse et le message se plaindra de l’indentation plutôt que du caractère. N’y mettez que des scalaires : la valeur d’un attribut ne peut pas contenir de structure, donc une table sous une clé @_ produit une bouillie convertie en chaîne plutôt que du XML imbriqué.

Le XML produit est-il valide ?

Il est bien formé, ce qui est une affirmation différente et plus faible. Chaque élément est fermé, il y a exactement une racine, l’esperluette, le signe inférieur et la séquence ]]> sont échappés dans le texte, les valeurs d’attributs échappent en plus le guillemet double et les caractères d’espacement, et une déclaration UTF-8 est écrite en tête.

La validité, c’est correspondre à un schéma, et YAML n’en porte aucun dont en dériver un. Si le XML part vers un endroit qui valide, apportez-le au validateur XSD avec le schéma que ce système publie.

Outils associés

Pour aller plus loin