Co se ztrácí při převodu mezi JSON, YAML, CSV a XML

Jak se JSON, YAML, CSV, XML a TOML liší v komentářích, typech, zanoření, datech a pořadí klíčů a které z těchto detailů každý převod mezi nimi tiše zahodí.

Každý převod mezi datovými formáty zahodí to, co zdroj vyjádřit uměl a cíl neumí. Některé ztráty jsou zjevné: komentáře zmizí ve chvíli, kdy se z YAML stane JSON. Většina je tichá. Datum dorazí jako řetězec, pole se promění v pět sloupců, ID, které končilo na 3, teď končí na 2. Užitečný návyk je vědět, který detail zmizí, ještě než zmizí.

Co který formát unese

KomentářeTypyZanořeníDataPořadí klíčůDuplicitní klíče
JSONNestring, number, boolean, null, object, arrayAnoNemá vlastní typPodle specifikace bez pořadí, většina parserů ho zachováNedefinované, obvykle vyhraje poslední
YAMLAno, #Typy JSON a navíc časová razítka a další v 1.1AnoAno v 1.1, v 1.2 podle taguZachované v souboru, po parsování neNeplatné, ale často se přijmou
CSVNeŽádné, každá buňka je textNe, jen ploché řádkyNeDané řádkem hlavičkyLegální a nejednoznačné
XMLAno, <!-- -->Bez schématu žádné, s XSD typovanéAnoJen přes schémaElementy mají pořadí, atributy neOpakované elementy jsou normální, duplicitní atributy jsou chyba
TOMLAno, #string, integer, float, boolean, date, time, date-time, array, tableAno, přes tabulkyAno, plnohodnotněNevýznamnéVždy chyba

Většinu překvapení způsobují dva sloupce. Typy rozhodují, jestli 007 přežije, nebo dorazí jako číslo 7, a zanoření rozhoduje, jestli je převod do CSV možný bez vymýšlení konvence.

CSV je ploché a každá buňka je text

CSV má jediný tvar: řádky polí. Objekt v buňce vyjádřit nejde, takže si převodník musí něco vymyslet. Obvykle to zploští cestami s tečkami, takže z tohohle:

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

se stane:

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

To se čistě rozbalí zpět, dokud nenastanou dvě věci. Pole různých délek rozšíří hlavičku na nejdelší záznam, takže jediný řádek se třemi značkami přidá sloupec tags.2 a všem ostatním řádkům nechá prázdnou buňku. A klíč, který sám obsahuje tečku, už poté nelze odlišit od zanořené cesty, takže cesta zpět postaví špatnou strukturu. Alternativou je řetězec JSON v buňce, "[""admin"",""ops""]" se zdvojenými uvozovkami, jak CSV vyžaduje, což neztratí nic a čemu dál v řadě nerozumí nic.

Druhou půlkou jsou typy. Buňka drží znaky, takže ten, kdo ji čte, musí hádat. Hádání dá 007 jako 7, 1.0 a 1 jako jednu hodnotu, PSČ bez úvodní nuly, firemní kód TRUE jako boolean. Nehádání dá každé číslo jako řetězec. Odvozování typů v převodu CSV do JSON nebo TSV do JSON je odhad s rozumnými výchozími hodnotami, ne obnova, protože původní typy nebyly nikdy zapsané.

Prázdná buňka může znamenat prázdný řetězec, null, nulu nebo nevztahuje se; rozhodněte, co znamená, a řekněte to, protože soubor to udělat nemůže. Výměna oddělovačů, jakou dělá převod CSV na TSV, je zde jediný bezpečný převod: nejsou tu žádné typy, které by šlo poškodit.

YAML za vás typ uhodne

YAML odvozuje typy z neuvozeného textu, což z něj dělá formát příjemný na psaní a rizikový na převod.

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

V parseru YAML 1.1, který PyYAML a mnoho starších knihoven stále implementují, je to:

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

Všechny čtyři změnily význam. no je norský problém: YAML 1.1 považuje y, yes, no, on a off za booleany, takže se z kódu země stane false. 1.20 je desetinné číslo, takže koncová nula je pryč. 010 odpovídá vzoru pro osmičkovou soustavu a stane se z něj 8. 12:30 odpovídá vzoru pro šedesátkovou soustavu a stane se z něj 750, tedy počet minut.

Parser s jádrovým schématem YAML 1.2, jako je současný js-yaml, čte tentýž soubor tak, že "no" je řetězec, 010 je 10 a "12:30" je řetězec. Jeden soubor, dva parsery, různá data. Specifikace se skutečně neshodují; verze 1.2 pravidla pro šedesátkovou soustavu a pro slovní booleany záměrně zrušila.

Obranou je jediný znak. Cokoli, co má být text, dejte do uvozovek: country: "no", version: "1.20". Převodník mezi YAML a JSON ukáže, co z toho souboru parser udělal, protože JSON nemá kam tu nejednoznačnost schovat.

JSON nemá data, nemá komentáře a má strop 53 bitů

Čísla v JSON jsou doubly podle IEEE 754, takže celá čísla zůstávají přesná jen do 2^53, což je 9007199254740992. Za touto hranicí:

{ "id": 9007199254740993 }

Naparsujte to a znovu serializujte ve většině jazyků a vrátí se to jako 9007199254740992. Ta hodnota se vůbec nedá vyjádřit, takže není ani tak zaokrouhlená jako spíš nedostupná. Právě proto Discord, Twitter a další API se snowflake identifikátory posílají ID jako řetězce: 64bitové celé číslo se do čísla v JSON nevejde.

Ani datový typ pro datum neexistuje, takže data cestují jako řetězce, obvykle v ISO 8601, a nic neoznačuje "2026-08-25" jako datum, a ne jako text. Přes TOML nebo přes parser YAML 1.1 se z toho může stát skutečné datum a pak se to vrátí naformátované jinak. JSON také nemá komentáře ani koncové čárky, takže poznámky ve zdroji v YAML nebo TOML jsou pryč, ne přesunuté. Generování rozhraní v TypeScriptu ze vzorku zdědí obě tato slepá místa: řetězec není datum a chybějící pole není volitelné.

XML se do JSON nevejde, ani v jednom směru

XML rozlišuje atributy od dceřiných elementů, zatímco JSON má jen klíče.

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

Převodník si zvolí konvenci, obvykle atributy uvozuje znakem @ a opakované elementy slučuje do pole. To je ztrátové zvláštním způsobem: u jediného <tag> žádné pole nevznikne, takže tvar výstupu závisí na datech, ne na schématu, a kód, který čeká seznam, se na záznamu s jednou značkou rozbije. Smíšený obsah jako <p>Hello <b>there</b> friend</p> nemá v JSON obdobu a jmenné prostory, CDATA a komentáře mizí.

Opačný směr ztrácí jiné věci. Jména elementů nesmějí začínat číslicí ani obsahovat mezery, takže se libovolné klíče z JSON musí zkomolit. Z null se konvencí stává prázdný element nebo xsi:nil. Pole na nejvyšší úrovni potřebuje obalový element, který ve zdroji nikdy nebyl. A nic v JSON nezaznamenává, jestli hodnota kdysi byla atributem, takže převod JSON do XML vypisuje jen elementy. Převod CSV do XML tuto volbu dělá otevřeně a ptá se na elementy, nebo atributy.

Pořadí klíčů a duplicitní klíče

Pořadí klíčů není zaručeno nikde a zachovává se skoro všude. Specifikace JSON označuje objekty za neuspořádané, a přesto každý běžný parser zachovává pořadí vkládání. U čehokoli, co se porovnává nebo se z toho počítá kontrolní součet, je řazení klíčů přesto bezpečnější, protože rozdíl dvou dokumentů, jimž se klíče jen přesunuly, se nedá číst. Strukturní porovnání, jaké dělá nástroj na porovnání JSON, páruje podle klíče, ne podle pozice, a tuto otázku obchází.

Ostřejší hranou jsou duplicitní klíče. JSON o nich neříká nic, takže se parsery liší a většina tiše zachová poslední hodnotu. YAML je zakazuje a mnoho parserů je stejně přijme. TOML je odmítá rovnou. CSV povoluje dva sloupce se stejnou hlavičkou a rozhodnutí nechává na tom, kdo soubor čte, což je dostatečný důvod, proč neznámý soubor nejdřív prohnat validátorem CSV. XML je jediné, kde je opakování smysluplné, a ne náhodné.

Převádět záměrně

Každá zpracovatelská řada má ztrátový krok. Vyberte si, který to bude, místo abyste na to přišli až později.

  • K nejužšímu formátu se dostaňte až nakonec. Cesta z XML do JSON a do CSV ztratí atributy v prvním kroku a zanoření ve druhém. Pokud je cílem CSV, rozhodněte, na kterých polích záleží, a zploštěte je záměrně.
  • Uvozujte před převodem, ne po něm. Čísla verzí, kódy zemí, PSČ a cokoli s úvodní nulou patří ve zdroji do uvozovek. Jakmile je no rovno false, původní text už se získat zpět nedá.
  • Zkontrolujte výstup jako data. Naformátujte ho, ověřte počet záznamů a prohlédněte si známý problematický řádek: čárku ve jméně, nejdelší pole, největší ID.
  • Porovnejte cestu tam a zpět. Převeďte ven, převeďte zpátky, porovnejte. Co porovnání ohlásí, to převod neunese.
  • Uchovejte originál. Převedený soubor je odvozenina, a když se za tři měsíce někdo zeptá, jestli bylo pole prázdné, nebo chybělo, odpovědět dokáže jen zdroj.