XML から YAML への変換

YAML へ変換。誤読される値は引用符で囲みます。

入力
出力
待機中文書を貼り付けると検査します。入力中にそのまま検証されます。

すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.

上に XML を貼り付けると、隣に YAML が現れます。文書は整形式かを検査され、木に写され、それからエミッターが書き出します。そのエミッターの主な仕事は、どの値に引用符が必要かを決めることです。何もアップロードされません。スキャナー、写し取り、エミッターはすべてこのタブで動きます。

これを欲しくなる理由はふつう、設定ファイルや Kubernetes のマニフェスト、CI のパイプライン、Ansible のインベントリーが、いま XML に入っているデータを必要としていることです。出力は機械が字句どおりに読むファイルに入るので、体裁より引用符が大事になります。

YAML は人懐っこい形式に見えて、データを黙って変えてしまう見込みがいちばん高い形式です。引用符のない国コード NO は、この生態系のほとんどで論理値 false になります。郵便番号 01730 は 1730 になります。バージョン 1.10 は 1.1 になります。このエミッターは、そのままでは読み違えられる値に引用符を付け、このページはどれをなぜ付けたのかを正確に述べます。

写し取りは XML から JSON への写し取りと同じ

YAML 1.2 は JSON の上位集合として設計されたので、ここに別の木はありません。XML は「XML から JSON」のページが作るのと同じ構造に変換され、別の直列化器がそれを書き出します。あのページの写し取りの決まりはすべてそのまま当てはまります。属性は接頭辞つきのキーになり、属性や子と同じ要素を分け合うテキストはテキスト用のキーの下に入り、2 回現れる要素は並びになり、コメントは落ちます。

1 つ悪くなることがあります。JSON なら受け取る側は少なくとも角かっこを見ます。YAML では、1 件と 2 件の違いは字下げされたスカラーとダッシュの並びの違いで、それを差分で見つける人はいません。考え方として一覧であるものには「常に配列」の欄を使ってください。1 件の文書と 50 件の文書が同じ形になります。

<order id="00042">
  <total currency="GBP">19.90</total>
  <line sku="0071">Widget</line>
  <line sku="0072">Gasket</line>
  <country>NO</country>
</order>

order:
  attr_id: '00042'
  total:
    attr_currency: GBP
    text: '19.90'
  line:
    - attr_sku: '0071'
      text: Widget
    - attr_sku: '0072'
      text: Gasket
  country: 'NO'
属性の接頭辞を attr_、テキストのキーを text に設定した場合。

ノルウェー問題と、それが覆う正確な一覧

YAML 1.1 は論理型を列挙で定義し、その列挙は誰の予想よりも広いものです。公開されている型のページは、そのまま次を挙げています。y、Y、yes、Yes、YES、n、N、no、No、NO、true、True、TRUE、false、False、FALSE、on、On、ON、off、Off、OFF。このどれもが、引用符なしでは論理値として読み込まれます。

その帰結には名前が付いています。ISO の国コードのデータはノルウェーに NO を与え、パーサーはアプリケーションに false を手渡します。同じ一覧は、表計算から書き出した Yes/No の列も、テキストのつもりだった on や off と書かれたスイッチも飲み込みます。YAML 1.2 は中核スキーマを true と false だけに狭めましたが、PyYAML、Ruby の Psych、Ansible、そして Kubernetes 周りの道具の多くはいまも 1.1 の集合を解決するので、全部が生きていると考えてください。

エミッターは、その一覧に正確に一致するスカラーを、1 文字の形も含めて単引用符でくくります。加えて null、Null、NULL、そしてチルダも。大文字小文字の区別に注意してください。yES や nO は 1.1 の一覧に入っていないので引用符は付きません。適合するパーサーもそれらを論理値として読まないからです。

ほかに引用符が付くもの、そしてすり抜けるもの

論理値の集合は有名な例であって、よくある例ではありません。壊れる値の多くは、もともと数ではなかった数です。YAML は、JSON がまさにしないやり方で、素のスカラーの綴りから型を推論するからです。スカラーが単引用符でくくられるのは、論理値か null の集合に一致するとき、JSON の数の文法に一致するとき(42、19.90、1.10 を含みます)、先頭のゼロのあとに数字が続くとき、空のとき、ハイフンやハッシュのような YAML の指示文字で始まるとき、そして前後どちらかに空白があるときです。

複数行のテキストに引用符は付きません。縦棒と切り取り指示で始まるリテラルのブロックスカラーになります。折りたたみではなくリテラルを選んでいるのは意図的です。折りたたみのブロックは単独の改行を空白に流し直してしまい、埋め込まれたコードや住所を壊します。切り取り指示は、ブロックがそのままでは足してしまう末尾の改行を取り除きます。

それでも引用符なしでエミッターを出ていき、下流で型が変わりうる値があります。ごまかさず並べておきます。素のスカラーを使うエミッターで YAML の型推論を解決しきったものは存在しないからです。

  • 60 進の数。YAML 1.1 は 22:22 を 60 進の整数として読むので、所要時間が PyYAML では 1342 になります。js-yaml のような 1.2 のパーサーは文字列を返すので、これはどちら側がファイルを読むかに左右されます。
  • 16 進の綴り。0x1F は YAML 1.1 でも 1.2 の中核スキーマでも 31 として読み込まれるので、16 進の色コードには引用符が必要です。
  • 日付。2024-01-05 は YAML のタイムスタンプ型に一致するので、js-yaml も PyYAML も文字列ではなく日付オブジェクトを渡してきます。
  • 最初の行が後続の行より深く字下げされたブロックスカラー。CDATA 節が先頭の空白を保っているときに起きます。YAML の直し方は縦棒のあとに明示的な字下げ指示を置くことですが、このエミッターはそれを書きません。

変換する前に属性の接頭辞とテキストのキーを設定する

やっておく価値のある下ごしらえはこれ 1 つです。既定値は JSON のために選ばれたもので、そこでは安全ですが、YAML はキーについて値より厳しい文法を持ちます。

既定の属性接頭辞は @_、既定のテキストキーは #text です。YAML では @ は予約された指示文字で、素のスカラーはそれで始められないので、@_id というキーは文書を解析不能にします。js-yaml は「bad indentation of a mapping entry」と報告し、PyYAML はどのトークンも始められない文字だと報告します。先頭の # はもっと厄介です。失敗しないからです。#text: 19.90 と書かれた行はコメントなので、ファイルは読み込まれ、値はただ存在しません。

どちらの欄もエディターの上の操作列にあります。接頭辞を attr_ のような素直なものにし、テキストのキーを text にすれば、出力のどのキーもふつうの YAML の名前になります。soap:Body のようにほかの理由で引用符が必要なキーは自動でくくられます。裸のキーにコロンは使えないからです。

コードで行う

手順は 2 つ。XML を安全に解析し、それから、きちんと引用符を付けるよう指示したダンパーで直列化します。XML 側にはいつもの実体のフラグが必要です。Java と .NET の既定は DOCTYPE を解決してしまうからです。YAML 側は注意が要ります。ダンパーによって引用符の付け方の強さが違うからです。

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

const parser = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: 'attr_',   // not @_: YAML reserves a leading @
  textNodeName: 'text',           // not #text: a leading # is a comment
  parseTagValue: false,           // keep values as strings
  parseAttributeValue: false,
  processEntities: false,         // do not expand DOCTYPE-declared entities
  isArray: (name) => ['line', 'item', 'entry'].includes(name),
});

const out = yaml.dump(parser.parse(xmlSource), {
  lineWidth: -1,     // never fold long lines; folding rewrites your data
  noRefs: true,      // never emit anchors and aliases
  quotingType: "'",
  sortKeys: false,
});

// js-yaml's dumper is conservative: it quotes NO, 01730, 1.10, 22:22 and
// 0x1F on its own, and quotes keys that begin with @ or #. Add
// forceQuotes: true if you want every string quoted regardless.
import xmltodict
import yaml

doc = xmltodict.parse(
    xml_source,
    disable_entities=True,          # blocks the expat entity attacks
    attr_prefix='attr_',
    cdata_key='text',
    force_list=('line', 'item', 'entry'),
)

print(yaml.safe_dump(
    doc,
    default_flow_style=False,
    allow_unicode=True,
    sort_keys=False,
    width=10 ** 9,                  # effectively disable line folding
))

# PyYAML implements the YAML 1.1 resolver, so its dumper knows that NO,
# 01730 and 1.10 would load back as a bool, an int and a float, and quotes
# them. Use safe_dump, never dump: the full dumper emits Python-specific
# tags that only yaml.unsafe_load can read back.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import javax.xml.stream.XMLInputFactory;

XMLInputFactory input = XMLInputFactory.newFactory();
input.setProperty(XMLInputFactory.SUPPORT_DTD, false);
input.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);

JsonNode tree = new XmlMapper(new XmlFactory(input)).readTree(xmlSource);

YAMLMapper yaml = YAMLMapper.builder()
    .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
    .disable(YAMLGenerator.Feature.MINIMIZE_QUOTES)   // off is the safe state
    .enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE)
    .build();

String out = yaml.writeValueAsString(tree);

// MINIMIZE_QUOTES is the setting to leave alone. It is off by default, and
// turning it on is how a value of NO ends up unquoted in a Jackson-generated
// file that a Python service then reads as false.
using System.Xml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YamlDotNet.Core;
using YamlDotNet.Serialization;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
    MaxCharactersFromEntities = 1024 * 1024,
};

using var reader = XmlReader.Create(new StringReader(xmlSource), settings);
var document = new XmlDocument { XmlResolver = null };
document.Load(reader);

string json = JsonConvert.SerializeXmlNode(document);
object? tree = JsonConvert.DeserializeObject<JObject>(json)?.ToObject<object>();

var serialiser = new SerializerBuilder()
    .WithDefaultScalarStyle(ScalarStyle.SingleQuoted)   // quote everything
    .Build();

Console.Write(serialiser.Serialize(tree));

// WithDefaultScalarStyle is blunt: every scalar comes out quoted, including
// the ones that did not need it. That is the right trade for generated data.
// Drop it only if you are hand-checking the output.
# yq v4 (Mike Farah) converts directly and quotes ambiguous scalars.
yq -p=xml -o=yaml '.' document.xml

# Match the key convention used on this page:
yq -p=xml -o=yaml \
   --xml-attribute-prefix='attr_' \
   --xml-content-name='text' \
   '.' document.xml > out.yaml

# Then load it back with the parser that will actually consume it. This is
# the only check that proves nothing changed type on the way through:
python -c "import yaml; print(yaml.safe_load(open('out.yaml'))['order']['country'])"
# expect: NO      not: False

このページが扱う失敗は静かです。引用符のない NO が入った YAML ファイルは、きれいに解析され、きれいに検証され、きれいに配備されます。ただその国はそれ以降 false です。これを捕まえる確認は、生成したファイルを受け取り側と同じライブラリで読み戻し、厄介だと分かっている値を見比べることであって、差分を読むことではありません。

よくある質問

YAML に変換するとき、私の XML はアップロードされますか。

いいえ。XML のスキャナー、木への写し取り、YAML のエミッターはすべてこのタブの JavaScript であり、送信先のエンドポイントもありません。開発者ツールのネットワークタブを開き、文書を貼り付けて、何も起きないのを見てください。

思い込まずに確かめる価値があります。YAML に変換される XML は、たいてい設定だからです。接続文字列、サービスアカウント、API キー、社内のホスト名は、どれも人がコンバーターに持ち込むたぐいの文書に行き着きます。

ノルウェー問題とは何ですか。

YAML 1.1 は論理型を綴りの固定された一覧として定義し、その一覧には n、N、no、No、NO が入っています。ですからノルウェーの ISO コードを持つフィールドを引用符なしで書くと、false として読み込まれます。同じ一覧は y と Y、on と off、そして表計算から書き出した Yes/No の列も飲み込みます。

YAML 1.2 は中核スキーマを true と false だけに狭めましたが、それで生態系が直ったわけではありません。PyYAML、Psych、Ansible、Kubernetes 周りの道具の多くはいまも 1.1 の集合を解決し、どのパーサーがあなたのファイルを読むかを選べることはまれです。エミッターはその一覧のすべての綴りに引用符を付けるので、NO は文字列 NO のままです。

ある値には引用符が付き、ある値には付かないのはなぜですか。

引用符が構造を支えているからです。素の YAML スカラーは綴りかたから型を推論されるので、01730 は数、1.10 は浮動小数、NO は論理値、先頭のハイフンは一覧の項目の始まりです。引用符は「これはテキストだ」と言う手段です。

エミッターは、そのままなら型や意味が変わる値にだけ引用符を付け、ほかは素のままにします。すべてのスカラーに引用符を付けると、得るものがないのにファイルが読みづらく差分も取りづらくなるからです。一様に引用符を付けたいなら、たいていの YAML ライブラリに強制引用のオプションがあります。上のサンプルは js-yaml と YamlDotNet についてそれを示しています。

複数行のテキスト内容はどうなりますか。

リテラルのブロックスカラーになります。縦棒と切り取り指示で始まり、その下に行が字下げされます。折りたたみではなくリテラルを選んだのは意図的です。折りたたみのブロックは単独の改行を空白に流し直し、埋め込まれたコードや住所を静かに壊します。

気をつける場合が 1 つ。テキストの最初の行が後続の行より深く字下げされていると、CDATA 節が先頭の空白を保っているときに起きることですが、そのブロックは曖昧になり、パーサーは受け取りを拒みます。

繰り返される要素は YAML の一覧になりますか。

はい。同じ親の下に 2 回以上現れる要素は、ダッシュの並びとして書かれる並びになります。1 回だけ現れるものは、素の入れ子の写像かスカラーになります。それは「XML から JSON」のページが述べている単数の曖昧さと同じもので、ここではもっと危ういものです。YAML はそれを隠すからです。1 件と 2 件の違いは、ダッシュ 1 つと空白 2 つぶんの字下げです。

エディターの上の「常に配列」の欄を使ってください。考え方として一覧である要素の名前を挙げれば、文書がそれを 1 つ持っていても 40 持っていても、並びとして出力されます。

XML のコメントと名前空間は保たれますか。

コメントは保たれません。文書を木に写す時点で、エミッターが何かを見る前に落ちます。YAML のコメントはデータモデルの一部ではないので、出力に書き込んだとしても、誰かが読み込んで保存し直した最初のときに消えてしまいます。

名前空間の接頭辞はそのまま残るので、soap:Body は soap:Body と綴られたキーになり、裸の YAML キーにコロンは使えないので自動でくくられます。「名前空間の接頭辞を取り除く」に印を付ければ素の Body になりますが、2 つの名前空間が 1 つのキーに統合される危険があります。

関連ツール

関連する解説