Wat er verloren gaat bij conversie tussen JSON, YAML, CSV en XML
Elke conversie tussen dataformaten laat vallen wat de bron kon uitdrukken en de bestemming niet. Sommige verliezen zijn duidelijk: commentaar verdwijnt op het moment dat YAML JSON wordt. De meeste zijn stil. Een datum komt aan als string, een array wordt vijf kolommen, een ID dat op 3 eindigde eindigt nu op 2. De nuttige gewoonte is weten welk detail zal verdwijnen voordat het verdwijnt.
Wat elk formaat kan dragen
| Commentaar | Types | Nesting | Datums | Sleutelvolgorde | Dubbele sleutels | |
|---|---|---|---|---|---|---|
| JSON | Nee | string, number, boolean, null, object, array | Ja | Geen eigen type | Ongeordend volgens de spec, door de meeste parsers behouden | Ongedefinieerd, meestal wint de laatste |
| YAML | Ja, # | die van JSON, plus timestamps en meer in 1.1 | Ja | Ja in 1.1, afhankelijk van de tag in 1.2 | Behouden in het bestand, niet na het parsen | Ongeldig, maar vaak geaccepteerd |
| CSV | Nee | Geen, elke cel is tekst | Nee, alleen platte rijen | Nee | Vastgelegd door de koprij | Toegestaan en dubbelzinnig |
| XML | Ja, <!-- --> | Geen zonder schema, getypeerd met XSD | Ja | Alleen via een schema | Elementen geordend, attributen niet | Herhaalde elementen normaal, dubbele attributen een fout |
| TOML | Ja, # | string, integer, float, boolean, date, time, date-time, array, table | Ja, via tabellen | Ja, volwaardig | Niet van betekenis | Altijd een fout |
Twee kolommen veroorzaken de meeste verrassingen. Types bepaalt of 007 overleeft of als het getal 7 aankomt, en nesting bepaalt of een conversie naar CSV mogelijk is zonder een conventie te verzinnen.
CSV is plat, en elke cel is tekst
CSV heeft één vorm: rijen met velden. Er is geen manier om een object in een cel uit te drukken, dus een converter moet iets verzinnen. Meestal vlakt hij af met paden met punten, dus dit:
[{ "id": 1, "name": { "first": "Ada" }, "tags": ["admin", "ops"] }]
wordt:
id,name.first,tags.0,tags.1
1,Ada,admin,ops
Dat laat zich netjes terugvouwen tot er twee dingen gebeuren. Arrays van verschillende lengte verbreden de kop tot het langste record, dus één rij met drie tags voegt een kolom tags.2 toe en laat bij elke andere rij een lege cel achter. En een sleutel die zelf al een punt bevat, is daarna niet meer van een genest pad te onderscheiden, dus de terugweg bouwt de verkeerde structuur op. Het alternatief is een JSON-string in de cel, "[""admin"",""ops""]" met de aanhalingstekens verdubbeld zoals CSV vereist, wat niets verliest en wat verderop niets begrijpt.
Types zijn de andere helft. Een cel bevat tekens, dus een lezer moet gokken. Gokken geeft 007 als 7, 1.0 en 1 als één waarde, een postcode zonder de voorloopnul, een bedrijfscode TRUE als boolean. Niet gokken geeft elk getal als string. Type-inferentie in CSV naar JSON, of in TSV naar JSON, is een gok met verstandige standaardwaarden en geen herstel, want de oorspronkelijke types zijn nooit vastgelegd.
Een lege cel kan lege string, null, nul of niet van toepassing betekenen; beslis wat het betekent en zeg het erbij, want het bestand kan dat niet. Scheidingstekens omwisselen, zoals CSV naar TSV doet, is hier de enige veilige conversie: er zijn geen types om te beschadigen.
YAML gokt het type voor je
YAML leidt types af uit tekst zonder aanhalingstekens, en dat is wat het prettig maakt om te schrijven en riskant om te converteren.
country: no
version: 1.20
build: 010
start: 12:30
Onder een YAML 1.1-parser, wat PyYAML en veel oudere bibliotheken nog steeds implementeren, is dat:
{ "country": false, "version": 1.2, "build": 8, "start": 750 }
Alle vier veranderden van betekenis. no is het Noorwegen-probleem: YAML 1.1 behandelt y, yes, no, on en off als booleans, dus de landcode wordt false. 1.20 is een float, dus de nul aan het eind is weg. 010 past op het octale patroon en wordt 8. 12:30 past op het sexagesimale patroon en wordt 750, een aantal minuten.
Een parser met het core schema van YAML 1.2, zoals het huidige js-yaml, leest hetzelfde bestand als de string "no", 010 als 10, en "12:30" als string. Eén bestand, twee parsers, verschillende data. De specificaties spreken elkaar werkelijk tegen; 1.2 heeft de regels voor sexagesimale getallen en booleanwoorden bewust laten vallen.
De verdediging is één teken. Zet alles wat tekst moet zijn tussen aanhalingstekens: country: "no", version: "1.20". De converter tussen YAML en JSON laat zien wat een parser van een bestand gemaakt heeft, omdat JSON de dubbelzinnigheid nergens kan verbergen.
JSON heeft geen datums, geen commentaar en een plafond van 53 bits
Getallen in JSON zijn IEEE 754 doubles, dus gehele getallen blijven alleen exact tot 2^53, oftewel 9007199254740992. Daarboven:
{ "id": 9007199254740993 }
Parse dat in de meeste talen en serialiseer het weer, en het komt terug als 9007199254740992. De waarde kan helemaal niet weergegeven worden, dus hij is minder afgerond dan wel onbeschikbaar. Daarom sturen Discord, Twitter en andere snowflake-API's ID's als strings: een 64-bits geheel getal past niet in een JSON-getal.
Er is ook geen datumtype, dus datums reizen als strings, meestal in ISO 8601, en niets markeert "2026-08-25" als datum in plaats van tekst. Via TOML of een YAML 1.1-parser kan het een echte datum worden, en vervolgens anders opgemaakt terugkomen. JSON kent ook geen commentaar en geen komma's aan het eind, dus aantekeningen in een YAML- of TOML-bron zijn verdwenen en niet verplaatst. TypeScript-interfaces uit een voorbeeld genereren erft beide blinde vlekken: een string is geen datum, en een ontbrekend veld is niet optioneel.
XML past niet in JSON, in geen van beide richtingen
XML onderscheidt attributen van onderliggende elementen, en JSON heeft alleen sleutels.
<user id="7" active="true">
<name>Ada</name>
<tag>admin</tag>
<tag>ops</tag>
</user>
Een converter kiest een conventie, meestal attributen voorafgaan door @ en herhaalde elementen samentrekken tot een array. Dat is op een specifieke manier lossy: bij één enkele <tag> is er geen array, dus de vorm van de uitvoer hangt van de data af in plaats van van het schema, en code die een lijst verwacht breekt op het record met één tag. Gemengde inhoud zoals <p>Hello <b>there</b> friend</p> heeft geen equivalent in JSON, en namespaces, CDATA en commentaar verdwijnen.
De omgekeerde weg verliest andere dingen. Elementnamen mogen niet met een cijfer beginnen of spaties bevatten, dus willekeurige JSON-sleutels moeten verminkt worden. null wordt per conventie een leeg element of xsi:nil. Een array op het hoogste niveau heeft een omhullend element nodig dat nooit in de bron stond. En niets in JSON legt vast of een waarde ooit een attribuut was, dus JSON naar XML levert alleen elementen op. CSV naar XML maakt die keuze open en vraagt om elementen of attributen.
Sleutelvolgorde en dubbele sleutels
Sleutelvolgorde wordt nergens gegarandeerd en vrijwel overal behouden. De JSON-specificatie noemt objecten ongeordend, en toch behoudt elke gangbare parser de invoegvolgorde. Sleutels sorteren is nog steeds veiliger voor alles wat vergeleken of van een checksum voorzien wordt, want een diff van twee documenten waarvan de sleutels alleen maar verplaatst zijn, is onleesbaar. Structureel vergelijken, zoals de JSON-vergelijkingstool doet, koppelt op sleutel in plaats van op positie en omzeilt de vraag.
Dubbele sleutels zijn de scherpere rand. JSON zegt er niets over, dus parsers verschillen en de meeste houden stilzwijgend de laatste waarde aan. YAML verbiedt ze en veel parsers accepteren ze toch. TOML wijst ze botweg af. CSV staat twee kolommen met dezelfde kop toe en laat de lezer beslissen, wat op zich al reden genoeg is om eerst een CSV-validator over een onbekend bestand te halen. XML is de enige waar herhaling betekenisvol is in plaats van toevallig.
Bewust converteren
Elke pijplijn heeft een lossy stap. Kies welke dat is in plaats van er later achter te komen.
- Bereik het smalste formaat als laatste. XML naar JSON naar CSV verliest attributen bij de eerste stap en nesting bij de tweede. Is CSV de bestemming, bepaal dan welke velden ertoe doen en vlak bewust af.
- Zet aanhalingstekens vóór het converteren, niet erna. Versienummers, landcodes, postcodes en alles met een voorloopnul horen in de bron tussen aanhalingstekens. Zodra
nofalseis, is de oorspronkelijke tekst niet meer terug te halen. - Controleer de uitvoer als data. Maak hem op, bevestig het aantal records, en bekijk een bekend lastig geval: de komma in een naam, de langste array, het grootste ID.
- Vergelijk een heen-en-terugweg. Converteer eruit, converteer terug, vergelijk. Wat de vergelijking meldt is wat de conversie niet kan dragen.
- Bewaar het origineel. Het geconverteerde bestand is een afgeleide, en als iemand over drie maanden vraagt of een veld leeg of afwezig was, kan alleen de bron antwoord geven.