YAML을 XML로 변환

YAML을 XML로, 전부 브라우저 안에서 변환합니다.

입력
출력
대기 중문서를 붙여넣으면 검사합니다. 입력하는 동안 검증이 실행됩니다.

모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.

위에 YAML을 붙여넣으면 옆에 들여쓴 정형식 XML이 나타납니다. 선언이 붙고 특수 문자는 모두 이스케이프됩니다. YAML이 파싱되지 않으면 빈 창 대신 파서 자신의 첫 불평 줄이 나오고, 적법한 XML 요소 이름으로 만들려고 키를 바꿔야 했다면 무엇이 바뀌었는지 목록으로 알려 줍니다.

오래된 것이 새것을 읽어야 할 때 필요한 방향입니다. XML만 받는 연동, SOAP 엔드포인트, XSD로 검증되는 교환 형식, YAML로 설정된 서비스를 위한 픽스처. 날 선 모서리가 놀랄 만큼 많고, 그 대부분은 XML이 아니라 YAML에서 옵니다.

파싱은 js-yaml이 하며, 사이트의 모든 페이지가 아니라 이 페이지를 열 때만 불려 옵니다. 모든 것이 이 탭에서 돌고 아무것도 업로드되지 않습니다. 이것이 중요한 이유는, YAML이 설정이 사는 곳이고 설정은 자격 증명이 사는 곳이기 때문입니다.

YAML의 세 가지 노드는 어떻게 옮겨지는가

YAML의 노드는 정확히 세 가지이고, 각각에 XML의 짝이 하나씩 있습니다. 사상은 키마다 하나씩의 자식 요소 묶음이 되고, 키가 요소 이름이 됩니다. 수열은 감싸는 그릇 없이 부모의 요소 이름을 구성원마다 한 번씩 되풀이합니다. XML이 목록을 나타내는 방법이 되풀이이기 때문입니다. 스칼라는 그 요소의 텍스트 내용이 됩니다.

그 위에 루트 문제가 얹힙니다. YAML은 문서 맨 위에 어떤 노드든 허용하지만, XML은 정확히 하나의 루트를 요구합니다. 키가 하나뿐인 사상에는 이미 자연스러운 루트가 있으므로 그 키가 루트 요소가 됩니다. 키가 둘 이상인 사상, 맨 위의 수열, 맨 스칼라는 하나의 요소로 감싸이며, 기본 이름은 root이고 조절 줄에서 바꿀 수 있습니다.

수열 규칙의 결과 둘은 알아 둘 값어치가 있습니다. 빈 수열은 아무것도 만들지 않으므로 키가 사라집니다. 요소를 0번 되풀이하는 것은 요소가 0개라는 뜻입니다. 그리고 다른 수열 안에 바로 중첩된 수열은 평평해집니다. 안쪽 수열에는 쓸 자기 이름이 없기 때문입니다.

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>
사상, 수열, 널, 그리고 빈 수열.

XML이 보기 전에 YAML이 이미 형을 정해 버렸다

이 페이지에서 가장 중요한 것이고, 이 변환기의 성질이 아닙니다. YAML은 맨 스칼라의 형을 적힌 꼴에 따라, 파서 안쪽에서 정합니다. 값이 XML 작성기에 닿을 때쯤 그것은 이미 숫자이거나 논릿값이거나 날짜이거나 문자열이며, XML에는 그 구별을 되찾을 형 체계가 없습니다.

js-yaml은 YAML 1.2 핵심 스키마에 타임스탬프 형을 더한 것을 구현하며, 결과는 다음과 같습니다. 값을 붙여넣어 한 줄씩 확인해 볼 수 있습니다.

  • true와 false는 논릿값이고 텍스트 true와 false로 적힙니다. yes와 no는 여기서는 문자열로 남지만, PyYAML이나 앤서블 같은 YAML 1.1 파서는 no를 false로 읽으므로, 같은 파일이라도 도구가 다르면 다른 XML이 나옵니다.
  • 앞의 0은 변환 전에 이미 사라집니다. 01730은 숫자 1730으로 풀리고, XML 작성기가 할 수 있는 일은 없습니다. '01730'이라고 쓰세요.
  • 다른 진법도 풀리므로 0x1F는 31이 되어 <hex>31</hex>로 적힙니다. 16진 색 코드와 하드웨어 식별자에는 인용부호가 필요합니다.
  • YAML의 정수는 브라우저에서 배정밀도가 되므로, 열아홉 자리 식별자는 작성기가 끼어들기도 전에 이미 아래 자릿수를 잃습니다. 식별자에는 언제나 인용부호를 붙이세요.
  • 날짜는 타임스탬프로 풀리고, 타임스탬프에는 작성기가 만들 수 있는 텍스트 표현이 없으므로 2024-01-05는 빈 <when/>으로 나옵니다. 인용부호를 붙이면 텍스트로 적힙니다.

YAML의 키는 적법한 XML 이름이 아닌 경우가 잦다

XML 1.0의 2.3절은 요소 이름이 글자나 밑줄, 콜론으로 시작하고 거기에 숫자와 붙임표와 점을 더한 것으로 이어진다고 말합니다. YAML의 키에는 그런 제약이 없습니다. 「2024 total」, 「user@email」, 그리고 빈 문자열은 모두 평범한 키이고, 어느 것도 요소 이름이 될 수 없습니다.

각각은 거부되지 않고 이름이 바뀌며, 바뀐 것은 모두 보고됩니다. 적법하지 않은 글자는 하나씩 밑줄로 바뀌고, 그래도 숫자로 시작하는 이름에는 앞에 밑줄이 붙습니다. 없애지 않고 바꾸는 것은 일부러입니다. 없애면 「2024 total」과 「2024total」이 같은 요소가 되어 서로 다른 두 필드가 합쳐집니다. 그래서 「2024 total」은 _2024_total이 되고, 「2024-total」은 붙임표가 이미 적법하므로 _2024-total이 되며, 「user@email」은 user_email이 됩니다. 빈틈이 없지는 않습니다. 「first name」과 「first_name」은 둘 다 first_name에 닿으므로, 문장부호만 다른 키는 이름을 바꿔 두세요.

YAML은 문자열이 아닌 키도 허용합니다. 2024는 정수, true는 논릿값이고, 명시 키 문법은 수열 전체를 키로 쓸 수 있게 합니다. 모두 요소 이름이 되기 전에 문자열로 바뀝니다. 한 가지 버릇이 있습니다. 배열 첨자처럼 보이는 키는 먼저, 그것도 숫자 오름차순으로 나열되므로, 2와 10과 name이 섞인 사상은 여러분이 적은 순서대로 요소를 내놓지 않습니다.

여러 문서 스트림, 앵커, 병합 키

YAML 스트림은 붙임표 셋으로 나뉜 여러 문서를 담을 수 있고, 쿠버네티스 매니페스트는 늘 그렇게 합니다. XML의 루트는 정확히 하나이므로, 모두 읽어 감쌉니다. 하나의 <documents> 요소 안에 YAML 문서마다 하나씩의 <document> 자식이 들어가고, 몇 개를 찾았는지 알리는 쪽지가 붙습니다. 대부분의 변환기는 조용히 첫 문서까지만 자르고, 그것을 알게 되는 것은 매니페스트의 3분의 2가 소리 없이 사라진 운영 환경에서입니다.

앵커와 별칭, 병합 키는 파서가 풀어내고, XML이 적힐 무렵에는 사라져 있습니다. 여러분이 얻는 것은 완전히 펼쳐진 결과이며, 그것은 옳고 입력보다 상당히 클 수 있습니다. 바탕 블록 하나를 마흔 개 서비스에 별칭으로 넣으면 사본 마흔 개가 생깁니다. 그 펼침은 이 탭의 메모리에서 일어나므로 별칭이 무거우면 느릴 수 있고, 크기 상한은 원본뿐 아니라 출력에도 적용됩니다.

주석은 보존되지 않습니다. YAML 데이터 모형의 일부가 아니고, 파서가 넘겨주지도 않기 때문입니다. 빈 스트림이나 주석만 담긴 스트림은 빈 루트 요소로 변환되는 대신 「비어 있음」으로 보고됩니다.

코드로 하기

어느 언어에서든 두 단계입니다. 안전한 로더로 YAML을 읽고, 그다음 제대로 이스케이프하는 것으로 XML을 씁니다. 여기서 안전을 위한 플래그는 XML 쪽이 아니라 YAML 쪽에 있습니다. 몇몇 YAML 라이브러리는 기본값으로, 또는 문서 안의 태그 하나로, 파일에서 임의의 클래스를 만들어 냅니다. 그것은 설정의 옷을 입은 원격 코드 실행입니다.

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 -

이들 가운데 어느 것에서든 제대로 맞춰야 할 것은 작성기가 아니라 로더입니다. 파이썬의 yaml.load, 자바 SnakeYAML의 기본 Constructor, 그리고 yaml.decode_php를 켠 yaml_parse는 모두 문서의 태그에서 임의의 객체를 만듭니다. YAML 파일은 데이터입니다. 그것이 다른 무엇이 될 수 있게 하는 로더를 쓰기 전까지는.

자주 묻는 질문

제 YAML이 어딘가로 업로드되나요?

아닙니다. YAML 파서도 XML 작성기도 이 탭에서 도는 자바스크립트이고, 그것들이 닿을 서버 쪽 구성 요소도 없습니다. 개발자 도구의 네트워크 탭을 열고 문서를 붙여넣으면, 페이지 자신의 자원이 한 번 불려 오고 그 뒤로는 아무것도 없는 것을 보게 됩니다.

여기서는 특히 확인할 값어치가 있습니다. YAML은 설정이 사는 곳입니다. 쿠버네티스 시크릿, CI 파이프라인 변수, 호스트 이름과 사용자 이름이 든 앤서블 인벤토리, 데이터베이스 비밀번호가 적힌 compose 파일.

제 우편번호나 버전 번호, ID가 왜 바뀌었나요?

XML 작성기가 아니라 YAML이 바꿨기 때문입니다. YAML은 스칼라의 형을 적힌 꼴에서 추론하므로 01730은 숫자 1730, 1.10은 실수 1.1이 되고, 열아홉 자리 식별자는 배정밀도에 들어가지 않습니다. 그 모든 일이 XML 관련 무엇이 돌기도 전에 YAML 파서 안에서 일어납니다.

고치는 곳은 YAML입니다. 값에 인용부호를 붙이세요. '01730', '1.10', '9007199254740993'은 모두 문자열로 도착해 적은 그대로 적힙니다. 그 YAML을 생성기가 만들었다면, 생성기가 인용부호를 붙였어야 합니다.

---로 나뉜 여러 문서가 든 YAML 파일은 어떻게 되나요?

모두 읽어 감쌉니다. 하나의 <documents> 요소 안에 YAML 문서마다 하나씩의 <document> 자식이 들어가고, 쪽지 패널이 몇 개를 찾았는지 알려 줍니다.

다른 길, 대부분의 변환기가 고르는 길은 첫 문서만 변환하고 나머지를 조용히 무시하는 것입니다. 쿠버네티스 매니페스트에는 나쁜 기본값입니다. 한 파일이 Deployment와 Service와 ConfigMap을 함께 담는 일이 흔하고, 셋 중 둘을 잃어도 배포가 실패하기 전까지는 알아차리지 못하기 때문입니다.

앵커와 별칭, 병합 키는 어떻게 다뤄지나요?

파서가 풀어내고, 출력에서는 완전히 펼쳐집니다. 앵커는 노드에 표시를 하고, 별칭은 그것을 되가리키며, 병합 키는 한 사상을 다른 사상에 적용합니다. 셋 다 XML에는 없고, 어느 것도 살아남지 않습니다.

얻는 것은 옳지만 입력보다 훨씬 클 수 있습니다. 바탕 블록 하나를 마흔 개 서비스에 별칭으로 넣으면 완전한 사본 마흔 개가 생깁니다. 그것이 그 YAML이 뜻한 바이고, 다만 YAML이 한 번만 쓰게 해 주었을 뿐입니다. 펼침은 이 탭의 메모리에서 일어나므로 별칭이 무거우면 느릴 수 있습니다.

값을 자식 요소가 아니라 XML 속성으로 낼 수 있나요?

있습니다. YAML의 키 앞에 조절 줄에 보이는 속성 접두사(기본값 @_)를 붙이세요. 값이 7인 '@_id' 키는 <id> 자식이 아니라 감싸는 요소의 id 속성이 됩니다.

그 키에는 인용부호를 붙여야 합니다. 맨 스칼라는 YAML이 예약한 @로 시작할 수 없으므로, 인용부호 없는 @_id는 파싱 오류가 되고, 게다가 메시지는 글자가 아니라 들여쓰기를 탓합니다. 거기에는 스칼라만 두세요. 속성값은 구조를 담을 수 없으므로, @_ 키 아래에 사상을 두면 중첩된 XML이 아니라 문자열로 뭉개진 엉망이 나옵니다.

만들어지는 XML은 유효한가요?

정형식입니다. 그것은 다르고 더 약한 주장입니다. 모든 요소가 닫히고, 루트는 정확히 하나이며, 텍스트 안의 앰퍼샌드와 작다 기호와 ]]> 차례는 이스케이프되고, 속성값에서는 겹인용부호와 공백 문자까지 이스케이프되며, 맨 위에 UTF-8 선언이 적힙니다.

유효하다는 것은 스키마에 맞는다는 뜻인데, YAML에는 그것을 끌어낼 스키마가 없습니다. 그 XML이 검증하는 곳으로 간다면, 그 시스템이 공개한 스키마와 함께 XSD 검사기로 가져가세요.

관련 도구

참고 자료