Co ginie przy konwersji między JSON, YAML, CSV i XML
Każda konwersja między formatami danych porzuca to, co źródło potrafiło wyrazić, a cel nie potrafi. Część strat jest oczywista: komentarze znikają w chwili, gdy YAML staje się JSON-em. Większość jest cicha. Data przychodzi jako ciąg znaków, tablica zamienia się w pięć kolumn, identyfikator kończący się na 3 kończy się teraz na 2. Przydatnym nawykiem jest wiedzieć, który szczegół zniknie, zanim zniknie.
Co każdy format potrafi unieść
| Komentarze | Typy | Zagnieżdżanie | Daty | Kolejność kluczy | Powtórzone klucze | |
|---|---|---|---|---|---|---|
| JSON | Nie | string, number, boolean, null, object, array | Tak | Brak typu natywnego | Wedle specyfikacji nieuporządkowana, zachowywana przez większość parserów | Niezdefiniowane, zwykle wygrywa ostatni |
| YAML | Tak, # | To co w JSON, a w 1.1 dodatkowo znaczniki czasu i więcej | Tak | Tak w 1.1, w 1.2 zależnie od tagu | Zachowywana w pliku, po sparsowaniu już nie | Niepoprawne, ale często akceptowane |
| CSV | Nie | Brak, każda komórka to tekst | Nie, tylko płaskie wiersze | Nie | Ustalona przez wiersz nagłówka | Dozwolone i niejednoznaczne |
| XML | Tak, <!-- --> | Brak bez schematu, typowanie przez XSD | Tak | Tylko przez schemat | Elementy uporządkowane, atrybuty nie | Powtórzone elementy są normalne, powtórzone atrybuty to błąd |
| TOML | Tak, # | string, integer, float, boolean, date, time, date-time, array, table | Tak, przez tabele | Tak, pełnoprawnie | Bez znaczenia | Zawsze błąd |
Większość niespodzianek biorą na siebie dwie kolumny. Typy rozstrzygają, czy 007 przetrwa, czy przyjdzie jako liczba 7, a zagnieżdżanie rozstrzyga, czy konwersja do CSV jest w ogóle możliwa bez wymyślania konwencji.
CSV jest płaski, a każda komórka to tekst
CSV ma jeden kształt: wiersze pól. Nie da się wyrazić obiektu w komórce, więc konwerter musi coś wymyślić. Zwykle spłaszcza dane za pomocą ścieżek z kropkami, więc to:
[{ "id": 1, "name": { "first": "Ada" }, "tags": ["admin", "ops"] }]
staje się tym:
id,name.first,tags.0,tags.1
1,Ada,admin,ops
Rozwija się to z powrotem czysto, dopóki nie zdarzą się dwie rzeczy. Tablice o różnych długościach rozszerzają nagłówek do najdłuższego rekordu, więc jeden wiersz z trzema tagami dokłada kolumnę tags.2 i zostawia w każdym innym wierszu pustą komórkę. A klucz, który już zawiera kropkę, staje się potem nie do odróżnienia od ścieżki zagnieżdżonej, więc droga powrotna odtwarza błędną strukturę. Alternatywą jest ciąg JSON w komórce, "[""admin"",""ops""]" z cudzysłowami podwojonymi tak, jak wymaga tego CSV, który niczego nie traci i którego nic dalej w łańcuchu nie rozumie.
Drugą połową sprawy są typy. Komórka mieści znaki, więc czytający musi zgadywać. Zgadywanie daje 007 jako 7, 1.0 i 1 jako jedną wartość, kod pocztowy bez wiodącego zera i kod firmy TRUE jako wartość logiczną. Rezygnacja ze zgadywania daje każdą liczbę jako ciąg znaków. Wnioskowanie typów przy konwersji CSV na JSON albo TSV na JSON jest zgadywaniem z rozsądnymi ustawieniami domyślnymi, a nie odzyskiwaniem, ponieważ pierwotne typy nigdy nie zostały zapisane.
Pusta komórka może oznaczać pusty ciąg znaków, wartość null, zero albo brak zastosowania; ustal, co oznacza, i powiedz o tym wprost, bo plik tego nie zrobi. Zamiana separatorów, jak przy konwersji CSV na TSV, jest tu jedyną bezpieczną konwersją: nie ma tam typów, które można by uszkodzić.
YAML zgaduje typ za ciebie
YAML wnioskuje typy z tekstu bez cudzysłowów, co czyni go przyjemnym w pisaniu i ryzykownym w konwersji.
country: no
version: 1.20
build: 010
start: 12:30
Dla parsera YAML 1.1, a taki wciąż implementuje PyYAML i wiele starszych bibliotek, jest to:
{ "country": false, "version": 1.2, "build": 8, "start": 750 }
Wszystkie cztery wartości zmieniły znaczenie. no to problem Norwegii: YAML 1.1 traktuje y, yes, no, on i off jako wartości logiczne, więc kod kraju staje się wartością false. 1.20 jest liczbą zmiennoprzecinkową, więc końcowe zero przepadło. 010 pasuje do wzorca ósemkowego i staje się liczbą 8. 12:30 pasuje do wzorca sześćdziesiątkowego i staje się liczbą 750, czyli liczbą minut.
Parser YAML 1.2 z podstawowym schematem, taki jak bieżący js-yaml, odczyta ten sam plik jako ciąg "no", 010 jako 10, a "12:30" jako ciąg znaków. Jeden plik, dwa parsery, różne dane. Specyfikacje naprawdę są ze sobą niezgodne; wersja 1.2 celowo porzuciła reguły sześćdziesiątkowe i regułę słów logicznych.
Obroną jest jeden znak. Bierz w cudzysłów wszystko, co ma być tekstem: country: "no", version: "1.20". Konwerter YAML i JSON pokazuje, co parser zrobił z plikiem, ponieważ JSON nie ma gdzie ukryć niejednoznaczności.
JSON nie ma dat, nie ma komentarzy i ma sufit 53 bitów
Liczby w JSON to liczby podwójnej precyzji IEEE 754, więc liczby całkowite pozostają dokładne tylko do 2^53, czyli do 9007199254740992. Powyżej:
{ "id": 9007199254740993 }
Sparsuj to i zapisz z powrotem w większości języków, a wróci jako 9007199254740992. Tej wartości w ogóle nie da się przedstawić, więc nie tyle została zaokrąglona, ile jest niedostępna. Właśnie dlatego Discord, Twitter i inne API oparte na identyfikatorach snowflake wysyłają identyfikatory jako ciągi znaków: 64 bitowa liczba całkowita nie mieści się w liczbie JSON.
Nie ma tam też typu daty, więc daty podróżują jako ciągi znaków, konwencjonalnie w ISO 8601, i nic nie oznacza "2026-08-25" jako daty, a nie jako tekstu. Przez TOML albo parser YAML 1.1 może stać się prawdziwą datą, a potem wrócić sformatowana inaczej. JSON nie ma również komentarzy ani przecinków na końcu list, więc adnotacje ze źródła w YAML albo TOML nie tyle są przenoszone, ile przepadają. Generowanie interfejsów TypeScript z próbki dziedziczy oba te martwe pola: ciąg znaków to nie data, a brakujące pole to nie pole opcjonalne.
XML nie mieści się w JSON, w żadną stronę
XML odróżnia atrybuty od elementów potomnych, a JSON ma tylko klucze.
<user id="7" active="true">
<name>Ada</name>
<tag>admin</tag>
<tag>ops</tag>
</user>
Konwerter wybiera konwencję, zwykle poprzedzając atrybuty znakiem @ i zwijając powtórzone elementy w tablicę. Jest to stratne w bardzo konkretny sposób: przy pojedynczym <tag> tablica nie powstaje, więc kształt wyniku zależy od danych, a nie od schematu, a kod oczekujący listy wywraca się na rekordzie, który ma jeden tag. Treść mieszana, taka jak <p>Hello <b>there</b> friend</p>, nie ma odpowiednika w JSON, a przestrzenie nazw, sekcje CDATA i komentarze znikają.
Droga powrotna traci co innego. Nazwy elementów nie mogą zaczynać się od cyfry ani zawierać spacji, więc dowolne klucze JSON trzeba przerobić. Wartość null staje się umownie pustym elementem albo xsi:nil. Tablica na najwyższym poziomie potrzebuje elementu opakowującego, którego w źródle nigdy nie było. I nic w JSON nie zapisuje, czy wartość była kiedyś atrybutem, więc konwersja JSON na XML wytwarza wyłącznie elementy. Konwersja CSV na XML podejmuje tę decyzję otwarcie, pytając o elementy albo atrybuty.
Kolejność kluczy i powtórzone klucze
Kolejność kluczy nie jest gwarantowana nigdzie i zachowywana prawie wszędzie. Specyfikacja JSON nazywa obiekty nieuporządkowanymi, a mimo to każdy powszechnie używany parser utrzymuje kolejność wstawiania. Sortowanie kluczy jest i tak bezpieczniejsze dla wszystkiego, co się porównuje albo opatruje sumą kontrolną, bo różnica dwóch dokumentów, w których klucze jedynie się przesunęły, jest nie do przeczytania. Porównywanie strukturalne, jak w narzędziu do porównywania JSON, dopasowuje po kluczu, a nie po pozycji, i omija to pytanie.
Powtórzone klucze są ostrzejszą krawędzią. JSON nic o nich nie mówi, więc parsery się różnią, a większość po cichu zachowuje ostatnią wartość. YAML ich zabrania, a i tak wiele parserów je przyjmuje. TOML odrzuca je wprost. CSV pozwala na dwie kolumny o tym samym nagłówku i zostawia decyzję czytającemu, co samo w sobie jest wystarczającym powodem, żeby najpierw przepuścić nieznany plik przez walidator CSV. XML jest jedynym formatem, w którym powtórzenie jest znaczące, a nie przypadkowe.
Konwersja z rozmysłem
Każdy potok ma krok stratny. Wybierz, który to będzie, zamiast odkrywać go później.
- Do najwęższego formatu docieraj na końcu. XML na JSON na CSV traci atrybuty w pierwszym kroku, a zagnieżdżenie w drugim. Jeśli celem jest CSV, zdecyduj, które pola mają znaczenie, i spłaszcz je świadomie.
- Cudzysłowy wstawiaj przed konwersją, a nie po niej. Numery wersji, kody krajów, kody pocztowe i wszystko z wiodącym zerem należy w źródle ująć w cudzysłowy. Gdy raz
nostanie się wartościąfalse, pierwotnego tekstu nie da się odzyskać. - Sprawdź wynik jako dane. Sformatuj go, potwierdź liczbę rekordów i obejrzyj znany kłopotliwy wiersz: przecinek w nazwisku, najdłuższą tablicę, największy identyfikator.
- Porównaj podróż w obie strony. Skonwertuj w jedną stronę, skonwertuj z powrotem, porównaj. To, co pokaże porównanie, jest tym, czego konwersja nie potrafi unieść.
- Zachowaj oryginał. Plik po konwersji jest utworem pochodnym, a kiedy ktoś zapyta za trzy miesiące, czy pole było puste, czy go nie było, odpowiedzieć może tylko źródło.