JSON ↔ YAML と XML 変換の境界
要点:JSON が YAML の部分集合だという主張は一方向しか保証せず、XML には公式の JSON マッピングがまったくありません。すべては慣習に乗っています。

要点:JSON を中間形式として使いましょう。ソースをデータモデルにパースし、それを対象形式にシリアライズします。変換前に、コメント、XML の属性と名前空間、数値や真偽値の型が生き残らなければならないかを決めます。
変換パイプラインが使えるかどうかは、出力がどれだけ美しいかではなく、どれだけ捨てたかに気づいているかで決まります。
1. JSON と YAML:一方向の包含
YAML 1.2 仕様ははっきり述べています。JSON は YAML の部分集合です。つまり、正しい JSON はすべて正しい YAML です。
{"name": "json", "tags": ["a", "b"], "count": 3}
この断片は JSON でもあり、YAML のフロースタイルでもあります。逆は成り立ちません。
| 機能 | JSON | YAML 1.2 |
|---|---|---|
| コメント | ✗ | ✓ |
| アンカーとエイリアス | ✗ | ✓ |
複数文書(---) |
✗ | ✓ |
| 非文字列キー | ✗ | ✓(1: a、true: b) |
| 複合キー | ✗ | ✓(? [a, b]) |
| インデント依存 | ✗ | ✓(タブのインデントは拒否される) |
| 型推論 | 明示的 | 暗黙的(0o17、0x1F、.inf) |
| 単一行の可読性 | 良い | 悪い(長い内容は折り畳み構文に頼る) |
1.1 型推論が最大の罠
YAML は文字列に見える一部の値を別の型にパースします。
zip: 01234 # 文字列か八進整数か。YAML 1.2 のコアスキーマでは文字列として扱う
version: 1.10 # 1.1(浮動小数点)。意味論が失われる
country: NO # YAML 1.2 では文字列。YAML 1.1 では真偽値 false
date: 2026-10-30 # 一部の実装は日付オブジェクトとしてパースする
NO の例は多くの人を焼いてきました。YAML 1.1 のパーサー(一部の古いライブラリ)では country: NO が false になります。
対策:「数値や真偽値に見える」識別子はすべて引用符で囲みます。version: "1.10" のように。
1.2 アンカー、エイリアス、複数文書
defaults: &defaults
retries: 3
timeout: 30
serviceA:
<<: *defaults
host: a.example.com
---
serviceB:
<<: *defaults
host: b.example.com
アンカーを展開して JSON に変換すると、通常は重複した二つの設定オブジェクトができ、または一つの配列になります(複数文書は [{...}, {...}] にならなければなりません)。
どちらにせよ、逆変換でアンカーは復元されません。この損失は避けられません。
1.3 コメントを保つ変換
ほとんどのライブラリは変換時にコメントを単純に落とします。残す必要があるときは yaml パッケージの Document API を使います。
import { parseDocument } from 'yaml';
const document = parseDocument(`
# サービス設定
port: 8080
`);
document.set('port', 3000);
console.log(document.toString());
// # サービス設定
// port: 3000
テストは単純です。このファイルが人間によって何度も編集されるなら、「パース → オブジェクト → シリアライズ」の経路を取らないでください。
1.4 往復一貫性の検証方法
/**
* JSON → YAML → JSON の往復一貫性を検証する。
* @param sourceValue 元の JSON 値
* @param stringify YAML シリアライズ関数
* @param parse YAML パース関数
* @returns 二つの結果が完全に等しいか
*/
function isRoundTripStable(
sourceValue: unknown,
stringify: (input: unknown) => string,
parse: (input: string) => unknown,
): boolean {
const yamlText = stringify(sourceValue);
const restored = parse(yamlText);
return JSON.stringify(restored) === JSON.stringify(sourceValue);
}
実データで実行する方が、ドキュメントを十回読むより価値があります。サンプル集合には次を必ず含めてください。null、空文字列、空配列、"00"、"1e5"、負のゼロ、2^53 より大きい整数、改行を含む長い文字列。
2. JSON と XML:標準マッピングは存在しない
XML は JSON よりずっと古く、構造能力(属性、名前空間、混合内容、DTD)がはるかに強いです。両者の間には公式のマッピング仕様がありません。どのツールも同じことをします。慣習を選び、できるだけ失わないようにするのです。
2.1 一般的な慣習
| 慣習 | 表現 | 例 |
|---|---|---|
| 要素 → オブジェクトのキー | 同名の要素がフィールドになる | <a>1</a> → {"a": "1"} |
属性 → @ 接頭辞 |
@name |
<a id="1"/> → {"a": {"@id": "1"}} |
テキスト → #text |
属性とテキストの両方がある場合 | <a id="1">hi</a> → {"a": {"@id": "1", "#text": "hi"}} |
| 繰り返し要素 → 配列 | 2 回以上で配列になる | <i>1</i><i>2</i> → {"i": ["1","2"]} |
| 名前空間 → 接頭辞 | ns:tag |
<ns:tag/> → {"ns:tag": ...} |
<order id="A1001">
<item sku="X1" qty="2">ノート</item>
<item sku="X2" qty="1">ペン</item>
</order>
{
"order": {
"@id": "A1001",
"item": [
{ "@sku": "X1", "@qty": "2", "#text": "ノート" },
{ "@sku": "X2", "@qty": "1", "#text": "ペン" }
]
}
}
2.2 避けられない五つの損失
- 要素の順序:JSON のオブジェクトは順序を持たず、XML は順序を持つ要素の並びです。
- 属性と子要素:XML では異なる意味論を持ちますが、JSON ではどちらも普通のフィールドになります。
- 混合内容:
<p>こんにちは <b>世界</b> さようなら</p>のような構造は、素のオブジェクトでは表現できません。 - 名前空間宣言:
xmlnsはスコープの概念です。フィールドに平坦化するとスコープが失われます。 - CDATA と実体参照:
<![CDATA[...]]>と&は JSON ではただのテキストにすぎません。
2.3 「1 要素か配列か」のジレンマ
<list><item>1</item></list>
item が一つのとき、xml2js は既定でオブジェクトを返し、二つになって初めて配列を返します。
その結果、フロントエンドのコードは Array.isArray(x) ? x : [x] と書かなければなりません。
対策:パース時に「常に配列」オプションを有効にするか、スキーマに基づいてフィールドを配列型として宣言します。
import { parseStringPromise } from 'xml2js';
const parsed = await parseStringPromise(xmlText, {
explicitArray: true, // 単一要素でも配列を返す
trim: true,
explicitRoot: false,
});
2.4 セキュリティ上の注意
信頼できない XML をパースする前に、外部実体を無効化しなければなりません。さもないと XXE(ローカルファイルの読み取り、内部ネットワークの探索)を引き起こす可能性があります。
// Node エコシステムでは、通常パーサーのオプションで DTD と外部実体を無効化する
const parserOptions = { doctype: false, externalEntities: false };
XML → JSON は変換の第一層にすぎません。変換しても XML が持ち込む攻撃面は消えません。
3. 三つの形式の使い分け
| 場面 | 推奨形式 | 理由 |
|---|---|---|
| 公開 HTTP API | JSON | ブラウザとすべての言語がネイティブ対応 |
| 人間が保守する設定 | YAML | コメント、インデント、可読性 |
| 強いスキーマと名前空間が必要な文書(SOAP、Office Open XML、SVG) | XML | 構造能力と既存エコシステム |
| 運用ツールとの連携(K8s、CI) | YAML | エコシステムの既定 |
| 永続化、キャッシュ、ログ | JSON / JSONL | 普遍的で依存がない |
4. 実装上の注意
import { stringify as toYaml } from 'yaml';
/**
* JSON 値を YAML テキストに変換し、日本語文字をエスケープせずに保つ。
* @param value 変換する JSON 値
* @returns YAML テキスト
*/
function jsonToYaml(value: unknown): string {
return toYaml(value, {
indent: 2,
lineWidth: 0, // 自動折り返しを無効化し、長い文字列が分割されないようにする
defaultStringType: 'QUOTE_DOUBLE',
});
}
実践的なオプション:
lineWidth: 0は自動折り返しを切ります。さもないと長い URL が行をまたいで分割され、読みにくくなります。- すべてを二重引用符で囲むと、
NOや1.10のような値が誤読されません。 - 数値精度が敏感なデータは、シリアライズ前に文字列へ変換します。
XML 方向では、単一のマッピング関数を固定し、「属性接頭辞」「テキストキー名」「配列戦略」をそのシグネチャに入れ、周辺に散らかさないようにします。
type XmlMappingOptions = {
attributePrefix: string;
textKey: string;
alwaysArray: boolean;
};
const defaultMapping: XmlMappingOptions = {
attributePrefix: '@',
textKey: '#text',
alwaysArray: false,
};
5. チェックリスト
- [ ] YAML 内の「数値/真偽値に見える」識別子をすべて引用符で囲んでいる。
- [ ] JSON → YAML → JSON の検証サンプル集合を定義し、テストでカバーしている。
- [ ] コメントを保つ必要があるとき、オブジェクトの往復ではなく Document API を使っている。
- [ ] XML マッピングの慣習を書き留め、チームが同じパラメータを共有している。
- [ ] 信頼できない XML をパースするとき、DTD と外部実体を無効化している。
- [ ] 変換前後でバイト数とフィールド数を比較し、異常があればアラートを出す。
FAQ
JSON、YAML、XML の変換方向はどう選べばよいですか?
JSON を中間形式として優先してください。YAML と XML をまずデータモデルにパースし、テキストを書き換えるのではなくそのモデルからシリアライズします。コメントを残す必要があるときは YAML の Document API を使い、属性や名前空間が必要になる前に XML のマッピング慣習を一つ固定してください。
XML から JSON への変換で属性と名前空間が失われるのはなぜですか?
JSON には属性や名前空間の概念がないので、XML の属性、接頭辞、名前空間 URI は普通のキーに畳み込む必要があります。一般には @ 接頭辞や #text ノードを使います。マッピング規則が分岐すると、同じ XML が異なる JSON を生み、往復できなくなります。
JSON、YAML、XML を長期的に双方向同期すべきですか?
いいえ。コメント、アンカー、XML の属性と名前空間は形式間で失われ、双方向同期は差異を積み重ね続けます。ソース形式を一つ選び、他は一度きりのエクスポートや一時的なビューとして扱ってください。