JSON から XML への変換
XML へ変換。何をなぜ改名したかが分かります。
すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.
上に JSON を貼り付けると、隣に整形式の XML が現れます。宣言つき、実際にインデントされ、特殊文字はすべてエスケープ済みです。JSON が解析できない場合は空のペインではなくパーサー自身のメッセージが出ますし、合法な XML 要素名にするためにキーを改名した場合は、どのキーが何になったかをツールが伝えます。
これが必要になるのは、下流の何かが XML しか話さないときです。SOAP のエンドポイント、レガシー ERP の取り込み、XSD で検証される交換フォーマット、モックしているサービス用のフィクスチャなど。対になる 2 方向のうち華やかでないほうであり、角が鋭いほうでもあります。XML は JSON にない制約を課し、その制約はどこかで必ず解決しなければならないからです。
すべてこのタブで動きます。アップロードはありません。これは明記する価値があります。人が変換器に貼り付ける JSON は、たいてい取得済みの API レスポンスであり、取得済みの API レスポンスにはトークンや口座番号、顧客レコードが入っているからです。
XML にはルートがちょうど 1 つ必要。JSON には不要。
RFC 8259 は JSON 文書の最上位にどんな値でも許します。オブジェクト、配列、文字列、数値、true、false、null。XML 1.0 は他のすべてを包むルート要素をちょうど 1 つ要求するので、このずれは変換のたびに解決されます。
キーがちょうど 1 つのオブジェクトにはすでに自然なルートがあるので、そのキーがルート要素になり、何も作り出しません。{"order": {...}} は包みなしで <order>...</order> になります。これはよくある形です。XML から JSON への変換が返す形そのものだからです。それ以外は包まれ、ノート欄がその旨を伝えます。
- 最上位のキーが 2 つ以上あるオブジェクトは、単一の要素に包まれます。既定の名前は root で、コントロール行から変更できます。
- 最上位が配列の場合は二重に包まれます。配列には自分の要素名がないので、各メンバーは <root> の中の <item> になります。
- 最上位がスカラーの場合はルート要素のテキストになるので、JSON 文書 42 は <root>42</root> になります。
- 最上位が null の場合は空のルート要素 <root/> になります。
配列は要素名を繰り返します。包みは付きません。
ここが、多くの変換器が逆にしてしまう判断です。{"line": ["a", "b"]} は <line> 要素 2 つが並ぶ形になり、<item> の子を 2 つ持つ <line> 要素にはなりません。繰り返しこそが XML におけるリストの表し方であり、XML から JSON への方向に単数形の問題がある理由でもあります。包みを作り出せば、既存のどのスキーマも受け付けない XML になりますし、往復もしません。
結果として 2 つのことが起きます。空の配列は何も生まないので、キーごと消えます。要素の繰り返しが 0 回なら要素は 0 個です。そして配列の配列は平坦になります。内側の配列には外側と区別される名前がないので、キー a の下の [[1,2],[3]] は <a> 要素 3 つになります。どちらかが問題になるなら、先に JSON の構造を作り直してください。
{
"order": {
"@_id": "00042",
"line": [ "Widget", "Gasket" ],
"note": null,
"meta": {},
"tags": []
}
}
<?xml version="1.0" encoding="UTF-8"?>
<order id="00042">
<line>Widget</line>
<line>Gasket</line>
<note/>
<meta/>
</order>JSON のキーは、しばしば合法な XML 名ではない
XML 1.0 の 2.3 節は Name を、NameStartChar のあとに NameChar が続くものと定義します。NameStartChar は文字、下線、またはコロンであり、数字でも空白でもアンパサンドでもドル記号でもありません。JSON のキーにはそうした制限がないので、"2024 total"、"user@email"、"$ref" はごく普通のキーであり、どれも合法な要素名ではありません。
.NET と XSD の世界はエスケープを選び、空白を _x0020_ にします。正確ですが読めません。このツールは代わりにサニタイズして報告します。不正な文字は削除ではなく 1 文字ずつ置換し、それでも数字で始まる名前には接頭辞を付けます。そこが要点です。削除は別々のキーを同じ名前に潰してしまいますが、置換はそうなりません。改名はすべてノート欄に出ます。
- "2024 total" は _2024_total に。空白が置換され、そのあと先頭の数字が接頭辞を強制します。
- "2024-total" は _2024-total に。ハイフンはもともと合法なので、接頭辞が要るのは先頭の数字だけです。2 つは別のまま残ります。削除していたら失われていたのがまさにこれです。
- "user@email" は user_email に、"$ref" は _ref に、空のキーは下線 1 文字になります。
- すでに合法なキーはそのまま通ります。コロンを含むものも同様で、"soap:Body" は "soap:Body" のままです。結果としてプレフィックス付きなのに xmlns 宣言のない要素になり、整形式ではあっても名前空間としては正しくありません。
属性、テキスト、そして JSON が真っ先に失うもの
@_ で始まるキーは、囲んでいる要素の属性になります。#text という名前のキーはテキスト内容を与えます。どちらも XML から JSON への方向と対応しているので、あちらのページの出力はそのまま元に戻せます。属性値はテキストより強くエスケープされます。&、<、" に加えて、タブ・改行・復帰を数値文字参照として書きます。XML 1.0 の 3.3.3 節が、再解析時に属性値中のリテラル空白を空白 1 個に正規化するからです。
2 つの損失は、このツールが関わる前に JSON の内部で起きており、しかも変換のバグのように見えます。JSON の数値は IEEE 754 の倍精度なので、19 桁の識別子を裸の数値として書いた時点で、テキストが解析されるころには下位の桁が失われています。また重複したキーはパーサーが解決し、最後のものが勝ちます。JavaScript 固有の癖もあります。配列インデックスに見えるキーは先に、しかも数値の昇順で列挙されるので、"2"、"10"、"name" を混ぜたオブジェクトは、書いた順に要素を出力しません。
null も空オブジェクトも <x/> になるので、両者は区別できず、どちらも空文字列として戻ってきます。区別が必要なら、標準に裏打ちされた「存在するが null」の言い方は xsi:nil="true" だけであり、祖先のどこかで xsi 名前空間を宣言しておく必要があります。
コードで同じことをする
XML を最もよく扱う 4 言語に PHP とシェルの 1 行を加えた、同じ変換です。セキュリティのフラグは戻り側でこそ効きます。JSON の解析自体は危険ではありませんが、JSON を XML に変換するコードは、ほぼ必ずどこかでその XML を再解析します。そして Java と .NET の既定値は、DOCTYPE が現れれば解決してしまいます。
import { XMLBuilder } from 'fast-xml-parser';
const builder = new XMLBuilder({
ignoreAttributes: false, // default is true: @_ keys would be dropped
attributeNamePrefix: '@_',
textNodeName: '#text',
format: true,
indentBy: ' ',
suppressEmptyNode: true, // write <note/> rather than <note></note>
processEntities: true, // escape &, < and " in values
});
const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' + builder.build(data);
// XMLBuilder does not sanitise keys. A key of "2024 total" is written
// verbatim and produces XML that will not parse, so check before building:
const illegal = Object.keys(flatten(data))
.filter((k) => !/^[A-Za-z_][\w.\-]*(:[A-Za-z_][\w.\-]*)?$/.test(k));
if (illegal.length) throw new Error('Illegal XML names: ' + illegal.join(', '));import json
import re
import xmltodict
def legal_name(key):
"""Replace illegal characters rather than stripping them, so that
distinct keys stay distinct. Prefix a leading digit."""
name = re.sub(r'[^\w.\-:]', '_', 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]
return node
data = json.loads(json_source)
if not isinstance(data, dict) or len(data) != 1:
data = {'root': data} # xmltodict.unparse requires a single root
print(xmltodict.unparse(
sanitise(data),
pretty=True, indent=' ',
attr_prefix='@_', cdata_key='#text',
full_document=True, # emit the <?xml ...?> declaration
))
# xmltodict raises ValueError("Document must have exactly one root.") rather
# than guessing, which is correct behaviour and the reason for the wrap.import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
JsonNode tree = new ObjectMapper().readTree(jsonSource);
XmlMapper xml = new XmlMapper();
xml.enable(SerializationFeature.INDENT_OUTPUT);
// JSON has no root name and Jackson will not invent one, so supply it.
String out = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ xml.writer().withRootName("root").writeValueAsString(tree);
// Two things Jackson will not do for you:
// 1. It does not sanitise names. A key with a space throws
// IllegalArgumentException at write time, which is at least loud.
// 2. It writes every value as a child element. There is no attribute
// convention on a JsonNode, so @_ keys become elements unless you bind
// to a class annotated with @JacksonXmlProperty(isAttribute = true).using System.Xml;
using Newtonsoft.Json;
// The second argument is the root element name, used when the JSON does not
// already have exactly one top-level property. Without it, multi-key JSON
// throws JsonSerializationException rather than producing invalid XML.
XmlDocument? document = JsonConvert.DeserializeXmlNode(jsonSource, "root");
if (document is null) throw new InvalidOperationException("Empty JSON.");
var settings = new XmlWriterSettings { Indent = true, IndentChars = " " };
using var writer = XmlWriter.Create(Console.Out, settings);
document.Save(writer);
// Json.NET uses "@" for attributes and "#text" for text, so retarget the
// keys if your JSON came from a converter using "@_". It does not sanitise
// names either: a property called "2024 total" throws XmlException("The ''
// character, hexadecimal value 0x20, cannot be included in a name").<?php
$data = json_decode($source, true, 512, JSON_THROW_ON_ERROR);
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) {
if (str_starts_with((string) $k, '@_')) {
$w->writeAttribute(legal_name(substr((string) $k, 2)), (string) $v);
} elseif ($k === '#text') {
$w->text((string) $v);
} else {
write_node($w, (string) $k, $v);
}
}
} elseif ($value !== null) {
$w->text(is_bool($value) ? ($value ? 'true' : 'false') : (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). JSON is a subset of YAML, so -p=json works directly.
yq -p=json -o=xml '.' payload.json
# Set the root and the key conventions to match this page:
yq -p=json -o=xml \
--xml-attribute-prefix='@_' \
--xml-content-name='#text' \
'{"root": .}' payload.json
# yq writes no XML declaration, so prepend one if a consumer expects it, and
# check the result: yq does not sanitise element names.
{ echo '<?xml version="1.0" encoding="UTF-8"?>'
yq -p=json -o=xml '{"root": .}' payload.json; } | xmllint --noout --nonet -ここに挙げたライブラリがどれもしないことに注目してください。キーを合法な XML 名にサニタイズすることです。Jackson、Json.NET、XMLBuilder は例外を投げるか、解析できない XML を出力し、yq は黙って出力します。JSON のキーがユーザー入力、データベースの列一覧、表計算のヘッダー行から来るなら、サニタイズの工程はあなたが書くものです。そして文字を削除せず置換することこそが、似た 2 つのキーが 1 つの要素になるのを防ぎます。
よくある質問
JSON はブラウザーの外へ出ますか。
いいえ。JSON パーサーも名前のサニタイザーも XML ライターも、すべてこのタブで動く JavaScript で、話しかけるサーバー側の仕組みがありません。ネットワークパネルを開いて何か変換してみてください。リクエストは 1 つも出ません。
この方向でこそ、それが最も重要です。変換器に貼り付けられる JSON は、たいていデバッグ中に生きた API から取得したレスポンスであり、アクセストークンや顧客レコード一式がそのまま入っています。この検索で上位に出るツールのいくつかはそのペイロードをサーバーへ送りますし、1 つは保存した文書を推測可能な URL で公開します。
なぜ JSON が <root> 要素に包まれるのですか。
XML がルート要素をちょうど 1 つしか許さず、あなたの JSON の最上位キーが複数だったか、配列だったか、裸のスカラーだったからです。XML で兄弟のルートを 2 つ書く方法はありません。
キーがちょうど 1 つのオブジェクトはそのままにします。そのキーがルートになり、包みは足されません。ですから {"order": {...}} は <order> になり、最上位キーを 1 つ足すと <root> になります。包みの名前はコントロール行で変更できます。XML の行き先が検証を行う場所なら、スキーマが期待する名前に合わせてください。
JSON の配列はどう変換されますか。
メンバーごとに要素名を繰り返し、包みは付けません。{"line": ["a", "b"]} は <line> 要素 2 つが並ぶ形になります。それが XML におけるリストの表し方であり、出力が往復できる理由です。変換器によっては <line><item>a</item><item>b</item></line> を出しますが、これは JSON には似て見えるものの、本物の XML 向けに書かれたどのスキーマでも検証に落ちます。
そこから 2 つの端のケースが出ます。空の配列は何も出さないのでキーごと消え、配列の直下にさらに配列があると平坦になります。内側には自分の名前がないからです。
有効な XML 要素名でないキーはどうなりますか。
改名され、改名はすべて出力の隣のノート欄に並びます。不正な文字は 1 文字ずつ下線に置き換えられ、それでも数字で始まる名前には先頭に下線が付きます。
削除ではなく置換なのは意図的です。削除すると "2024 total" と "2024total" が同じ要素になり、別々の項目が 1 つに統合されてしまいます。ただし完全な単射ではありません。"first name" と "first_name" はどちらも first_name になります。後者ではもともと下線が合法だったからです。
子要素ではなく属性にするには。
キーの先頭に @_ を付けてください。{"user": {"@_id": "7", "name": "Alice"}} は <user id="7"><name>Alice</name></user> になります。接頭辞はエディター上部で変更でき、空にすれば属性として書かれるものはなくなります。
そこに置くのはスカラーだけにしてください。値は文字列化されるので、@_ キーの下にオブジェクトを置くと、役に立たない [object Object] というテキストになります。このライターが多くの実装と違う点が 1 つあります。属性値の中のタブ・改行・復帰を数値文字参照として書くので、複数行の値が空白に潰されず再解析を生き延びます。
JSON に戻せば、元どおりになりますか。
ほとんどの文書では戻ります。XML から JSON のページで、@_ と #text を同じ設定にしてください。この既定値が選ばれているのは、その組み合わせのためです。
4 つは生き残りません。null と {} はどちらも <x/> になり、空文字列として戻ります。空の配列は完全に消えます。XML に型がないので JSON の数値型は失われ、型変換をオンにしない限り 42 は "42" として戻ります。そしてキーの順序はどちらの形式でも意味を持ちません。厳密な往復が要件なら、XML を正とみなし、XPath で読み出してください。