JSON、YAML、CSV、XML の相互変換で失われるもの

JSON、YAML、CSV、XML、TOML がコメント、型、入れ子、日付、キーの順序についてどう違うのか、そして相互変換のたびにどの情報が黙って捨てられるのかを解説します。

データフォーマット間の変換は必ず、変換元が表現できて変換先が表現できないものを落とします。分かりやすい損失もあります。YAML が JSON になった瞬間、コメントは消えます。しかし、ほとんどの損失は静かに起こります。日付が文字列として届き、配列が 5 つの列になり、3 で終わっていた ID が 2 で終わるようになります。役に立つ習慣は、何が失われるのかを失われる前に知っておくことです。

各フォーマットが運べるもの

コメント入れ子日付キーの順序重複キー
JSON不可string、number、boolean、null、object、array可能専用の型なし仕様上は順序なし。ほとんどのパーサーは保持未定義。通常は最後が勝つ
YAML可能。#JSON の型に加え、1.1 ではタイムスタンプなど可能1.1 では可能。1.2 ではタグ次第ファイル内では保持。解析後は保持されない不正だが受け付けられることが多い
CSV不可なし。すべてのセルはテキスト不可。平坦な行のみ不可ヘッダー行で固定妥当だが曖昧
XML可能。<!-- -->スキーマなしではなし。XSD で型付け可能スキーマ経由のみ要素には順序あり、属性にはなし要素の繰り返しは普通。属性の重複はエラー
TOML可能。#string、integer、float、boolean、date、time、date-time、array、table可能。テーブル経由可能。第一級の型意味を持たない常にエラー

驚きの大半は 2 つの列から生じます。007 がそのまま残るのか数値の 7 になるのかを決め、入れ子 は独自の取り決めを持ち込まずに CSV へ変換できるかどうかを決めます。

CSV は平坦で、すべてのセルはテキスト

CSV の形は 1 つだけです。フィールドの並んだ行です。セルの中にオブジェクトを表現する方法はないので、コンバーターは何かを考案するしかありません。たいていはドット区切りのパスで平坦化します。つまり次のものが、

[{ "id": 1, "name": { "first": "Ada" }, "tags": ["admin", "ops"] }]

こうなります。

id,name.first,tags.0,tags.1
1,Ada,admin,ops

これは、2 つのことが起きるまではきれいに元へ戻せます。1 つは長さの異なる配列です。ヘッダーは最も長いレコードに合わせて広がるため、タグが 3 つある行が 1 つあるだけで tags.2 の列が加わり、他のすべての行には空のセルが残ります。もう 1 つは、もともとドットを含むキーです。変換後は入れ子のパスと区別がつかないため、戻したときに誤った構造が組み上がります。代わりの手はセルに JSON の文字列を入れることで、CSV の規則どおりクォートを重ねて "[""admin"",""ops""]" と書きます。これは何も失いませんが、下流の何もこれを理解してくれません。

残る半分は型です。セルが保持しているのは文字なので、読み手は推測するしかありません。推測すれば 007 は 7 になり、1.01 は同じ値になり、郵便番号は先頭のゼロを失い、TRUE という会社コードは真偽値になります。推測しなければ、すべての数値が文字列になります。CSV to JSON や TSV to JSON における型推論は、復元ではなく、それらしい既定値に基づく推測です。元の型はどこにも書かれていないからです。

空のセルは、空文字列、null、ゼロ、該当なしのいずれをも意味しえます。何を意味するかを決め、それを明示してください。ファイル自身にはそれができません。CSV to TSV のように区切り文字を入れ替えるのは、ここで唯一安全な変換です。壊すべき型がそもそも存在しないからです。

YAML は型を勝手に推測する

YAML はクォートされていないテキストから型を推論します。これが YAML を書きやすくし、同時に変換を危うくしています。

country: no
version: 1.20
build: 010
start: 12:30

PyYAML をはじめ多くの古いライブラリが今も実装している YAML 1.1 のパーサーでは、これは次のようになります。

{ "country": false, "version": 1.2, "build": 8, "start": 750 }

4 つとも意味が変わっています。no はいわゆるノルウェー問題です。YAML 1.1 は yyesnoonoff を真偽値として扱うため、国コードが false になります。1.20 は浮動小数点数なので、末尾のゼロが消えます。010 は 8 進数のパターンに一致して 8 になります。12:30 は 60 進数のパターンに一致して 750、つまり分の数になります。

現在の js-yaml のような YAML 1.2 のコアスキーマのパーサーは、同じファイルを読んで no を文字列の "no"0101012:30 を文字列として解釈します。1 つのファイル、2 つのパーサー、異なるデータです。仕様そのものが食い違っているのです。1.2 は 60 進数の規則と真偽値の語の規則を意図的に廃止しました。

防御手段は 1 文字です。テキストとして扱いたいものはクォートしてください。country: "no"version: "1.20" のように書きます。YAML と JSON のコンバーターを使えば、パーサーがそのファイルをどう解釈したかが見えます。JSON には曖昧さを隠す場所がないからです。

JSON には日付もコメントもなく、53 ビットの上限がある

JSON の数値は IEEE 754 の倍精度浮動小数点数なので、整数が正確なのは 2^53、つまり 9007199254740992 までです。それを超えると次のようになります。

{ "id": 9007199254740993 }

ほとんどの言語でこれを解析して再度直列化すると、9007199254740992 として返ってきます。この値はそもそも表現できないので、丸められたというより、扱えないのです。Discord や Twitter をはじめとする snowflake 方式の API が ID を文字列で送るのは、これが理由です。64 ビット整数は JSON の数値に収まりません。

日付の型もないため、日付は文字列として運ばれます。慣習的には ISO 8601 ですが、"2026-08-25" がテキストではなく日付であることを示すものは何もありません。TOML や YAML 1.1 のパーサーを経由すると本物の日付になり、その後は別の書式で戻ってくることがあります。JSON にはコメントも末尾のカンマもないので、YAML や TOML の元ファイルにあった注釈は、移動するのではなく消えます。サンプルから TypeScript のインターフェースを生成すると、この 2 つの死角をそのまま引き継ぎます。文字列は日付ではありませんし、存在しないフィールドは省略可能とは限りません。

XML と JSON は、どちらの向きにも収まらない

XML は属性と子要素を区別しますが、JSON にはキーしかありません。

<user id="7" active="true">
  <name>Ada</name>
  <tag>admin</tag>
  <tag>ops</tag>
</user>

コンバーターは何らかの取り決めを選びます。たいていは属性に @ を前置し、繰り返される要素を配列にまとめます。この方式には特有の損失があります。<tag> が 1 つしかない場合には配列にならないため、出力の形はスキーマではなくデータに依存し、リストを期待しているコードはタグが 1 つのレコードで壊れます。<p>Hello <b>there</b> friend</p> のような混在内容には JSON での等価物がなく、名前空間、CDATA、コメントは消えます。

逆方向では別のものが失われます。要素名は数字で始められず、空白も含められないため、任意の JSON のキーは変形せざるをえません。null は慣習的に空要素か xsi:nil になります。トップレベルの配列には、元のファイルには存在しなかったラッパー要素が必要になります。そして JSON には、ある値がかつて属性だったかどうかを記録する仕組みがないため、JSON to XML は要素だけを出力します。CSV to XML はこの選択を明示的に扱い、要素と属性のどちらにするかを尋ねます。

キーの順序と重複キー

キーの順序はどこでも保証されていない一方で、ほぼどこでも保持されています。JSON の仕様はオブジェクトを順序なしと定めていますが、主要なパーサーはいずれも挿入順を保ちます。それでも、比較したりチェックサムを取ったりするものについては、キーを並べ替えておくほうが安全です。キーが動いただけの 2 つの文書の差分は、読めたものではないからです。JSON の比較ツールのように構造で比較すれば、位置ではなくキーで対応付けるので、この問題そのものを避けられます。

重複キーはもっと鋭い刃です。JSON は何も定めていないためパーサーごとに挙動が異なり、多くは黙って最後の値を採用します。YAML は禁止していますが、多くのパーサーはそれでも受け付けます。TOML は明確に拒否します。CSV は同じ見出しを持つ 2 つの列を許し、判断を読み手に委ねます。見慣れないファイルにまず CSV バリデーターをかけるべき理由としては、これだけで十分です。繰り返しが偶然ではなく意味を持つのは、XML だけです。

意図をもって変換する

どのパイプラインにも損失を伴う工程があります。後から発見するのではなく、どれをその工程にするかを自分で選んでください。

  • 最も表現力の狭いフォーマットには最後に到達する。 XML から JSON、そして CSV という経路では、最初の段階で属性が失われ、次の段階で入れ子が失われます。CSV が目的地なら、どのフィールドが重要かを決めた上で、意図的に平坦化してください。
  • クォートは変換の後ではなく前に付ける。 バージョン番号、国コード、郵便番号、先頭にゼロが付くものは、すべて元のファイルの時点でクォートに入れておきます。no がいったん false になってしまえば、元のテキストは復元できません。
  • 出力をデータとして確認する。 整形し、レコード数を確認し、扱いにくいと分かっている行を調べます。名前に入ったカンマ、最も長い配列、最も大きな ID などです。
  • 往復の差分を取る。 変換し、戻し、比較します。比較結果に現れるものが、その変換で運べないものです。
  • 元のファイルを残す。 変換後のファイルは派生物です。3 か月後に誰かが、あるフィールドは空だったのか存在しなかったのかと尋ねてきたとき、答えられるのは元のファイルだけです。