JSON do CSV: skąd bierze się nagłówek i dlaczego zmieniają się wartości
Podsumowanie: Dziewięć eksperymentów na prawdziwym konwerterze: strategia „klucze z pierwszego wiersza" cicho gubi pola, null, brak i pusty string stają się wszystkim pustymi komórkami, liczba całkowita o 20 cyfrach traci precyzję już w JSON.parse, a kropka w kluczu sprawia, że dwa różne ścieżki kolidują w tej samej kolumnie. Jak powstaje nagłówek, które wartości się deformują i jak wybrać strategię według odbiorcy danych.

Gdy konwersja JSON do CSV gubi dane, prawie nigdy nie dzieje się to w kroku „konwersji". Strata powstaje w dwóch pytaniach, których nikt nie zadał: kto zdecydował o nagłówku i czy każda komórka pozostaje rozpoznawalna jako ta wartość, którą była?
Pole znika w trzydzieści sekund
Wiele konwerterów — w tym idiomatyczny pipeline jq — bierze nagłówek tylko z pierwszego rekordu. Odtwórz konsekwencje w Pythonie:
data = [{"id": 1, "name": "A"}, {"id": 2, "email": "b@x.com"}]
first_keys = list(data[0].keys()) # ['id', 'name']
rows = [[r.get(k, "") for k in first_keys] for r in data]
# E-mail drugiego rekordu znika bez błędu i ostrzeżenia
Klucza email nie ma w nagłówku: nie ma on nawet prawa stać się pustą komórką — kolumna nigdy nie powstała, a wartość jest cicho wyrzucana. Poprawka to unia kluczy: przejść po wszystkich rekordach i utworzyć kolumnę dla każdego klucza, który pojawi się gdziekolwiek. Te same dane przez unię dają nagłówek id,name,email, z pustą komórką tam, gdzie brakuje name — i nic nie ginie.
Konwerter bezpośredni na naszej stronie /convert/csv stosuje dokładnie tę strategię. Dla wejścia [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}] daje:
id,name,email
1,A,
2,,b@x.com
Klucze są uporządkowane według pierwszego wystąpienia, a brakujące pola stają się pustymi komórkami. jsonoperations i jsonbeam nazywają obie to „poprawką klasycznego buga" — ale „używa unii" to tylko połowa historii. Unia ma swoją cenę; to następna sekcja.
Trzy sposoby zbudowania nagłówka — i komu który pasuje
| Strategia nagłówka | Jak powstaje | Siła | Cena |
|---|---|---|---|
| Klucze z pierwszego wiersza | Czytany jest tylko pierwszy rekord | Mało kolumn, stabilnie, jedna linijka jq | Nowe pola kolejnych rekordów gubią się po cichu |
| Unia kluczy | Czytane są wszystkie rekordy | Żadne pole nie ginie | Rzadkie kolumny i dużo pustych, gdy formy się różnią |
| Jawna lista kolumn | Zdefiniowana ręcznie lub przez schemat | W pełni kontrolowalna, odtwarzalna | Wymaga utrzymania; nowe pola wymagają ręcznej aktualizacji |
Kryterium wyboru nie brzmi „które jest bardziej wyrafinowane", lecz co odbiorca zrobi z tym CSV:
- Kolega, który rzuci okiem: klucze z pierwszego wiersza wystarczą, formy rekordów bywają zwykle jednorodne.
- Raport albo pipeline danych: unia kluczy to minimum — pusta komórka sama jest informacją.
- Cykliczny automatyczny eksport: jawna lista kolumn. Dryf formy (pole dodane po stronie źródła, zmieniona nazwa klucza) musi głośnie padać, a nie po cichu produkować tabelę, której nikt nie zauważa jako błędnej.
Trzecia opcja jest najbardziej zaniedbywana. W automatyzacji „konwersja się powiodła" i „konwersja jest poprawna" to dwa różne zdania — a jawna lista kolumn to jedyny mechanizm, który je rozdziela.
Gdy klucz zawiera kropkę, ścieżki spłaszczania kolidują
Spłaszczanie w notacji kropkowej zamienia {"a": {"b": 2}} w kolumnę a.b. Ale co, gdy klucz sam zawiera kropkę? Sprawdzone na pandas:
import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# Przetrwa tylko jedna kolumna: ['a.b'] — dwa zupełnie różne klucze kolidują na tej samej ścieżce
Dosłowny klucz a.b i zagnieżdżona ścieżka a → b dają tę samą nazwę kolumny, jedna nadpisuje drugą, a dane cicho się psują. To skonstruowany przykład, ale zdarza się na prawdziwych danych: nazwy metryk monitoringu (cpu.usage) i pola logów (error.type) kochają kropki.
Dwa remedia:
- Zmień separator.
sep="_"albo własny separator — po sprawdzeniu, że oryginalne klucze nigdy go nie zawierają. - Zmień notację ścieżek. JSON Pointer używa
/jako separatora (/a/b); konwerter Excel toolvore idzie tą drogą. Ma tę samą klasę niejednoznaczności — klucz z/zapisuje się jako~1— ale reguły escapowania są znormalizowane w RFC 6901, a nie wymyślane osobno przez każde narzędzie.
Cokolwiek wybierzesz, zapisz regułę w dokumentacji eksportu. Nagłówek CSV to jedyny „schemat", jaki niosą te dane; czytelnik nigdzie indziej nie znajdzie Twoich reguł spłaszczania.
Tablice: trzy strategie, każda gubi coś innego
Tablice to prawdziwa przeszkoda na drodze JSON do CSV: komórka nie może zawierać wielu wartości. Trzy strategie, z przegranymi zweryfikowanymi w praktyce:
| Strategia | Kształt wyjścia | Co się gubi | Idealne dla |
|---|---|---|---|
| JSON w komórce | Tablica zserializowana jako string JSON w komórce | Narzędzia tabelaryczne nie mogą filtrować ani agregować elementów | Wierność najpierw, ręczna inspekcja |
| Kolumny indeksowane | tags.0, tags.1, po jednej kolumnie |
Liczba kolumn wybucha wraz z najdłuższą tablicą; dużo pustych | Małe tablice o stałej długości |
| Rozwinięte wiersze | Jeden wiersz na element, pola rodzica zduplikowane | Inflacja wierszy; zachowanie pustych tablic trzeba określić osobno | Analiza szczegółów, agregacja po elemencie |
Nasze konwertery /convert/csv i /convert/table używają JSON w komórce. Wejście [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}] daje:
orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"
Gdy potrzebujesz rozwiniętych wierszy, pandas record_path robi to wprost:
orders = [{"orderId": "A001", "total": 99,
"items": [{"sku": "X1", "qty": 2}, {"sku": "Y9", "qty": 1}]}]
items = pd.json_normalize(orders, record_path=["items"],
meta=["orderId", "total"], sep=".")
# Dwa wiersze szczegółów; orderId i total zduplikowane w każdym
W rozwijaniu są trzy pułapki do rozstrzygnięcia zawczasu: rozwinięcie kilku tablic naraz buduje iloczyn kartezjański; puste tablice domyślnie nie dają żadnego wiersza (Twoja lista klientów traci ludzi); a zduplikowane pola rodzica wymagają deduplikacji przy agregacji po orderId po stronie odbiorcy. Poradnik convertcsvonline wymienia „najpierw ustal granularność" jako pierwszy krok dokładnie dlatego — gdy granularność jest ustalona, każde z trzech pytań ma jedną odpowiedź.
Przetestowane: wartości, które wychodzą z konwersji zmienione
Test wierności wykonany na naszym prawdziwym konwerterze (Node v22.13.1 + papaparse, ta sama implementacja co /convert/csv):
1. null, brak i pusty string stają się jednakowymi pustymi komórkami. Wejście [{"a":null},{"b":""},{"c":1}] — wszystkie trzy komórki wychodzą puste. CSV nie ma trzech stanów, tylko „są znaki" i „nie ma znaków". Po round-trip wszystkie trzy czytają się jako ""; pierwotna semantyka jest nieodwracalna.
2. Zagnieżdżone obiekty i tablice stają się stringami JSON. Cudzysłowy są podwajane według RFC 4180 ({""name"":""张三""}), a po wklejeniu komórki w dowolny parser JSON wartość wraca — dlatego JSON w komórce zachowuje wierność.
3. Liczba całkowita o 20 cyfrach traci precyzję już przy parsowaniu — i winne nie jest CSV. Wejście "id": 9007199254740993; w CSV jest już 9007199254740992. JSON.parse w JavaScript używa double'ów IEEE 754: strata zdarza się przy parsowaniu, przed jakimkolwiek konwerterem CSV. Python json.load zachowuje ją w całości. Pola-identyfikatory (ID snowflake, numery zamówień) powinny przychodzić jako stringi już od producenta. Więcej o precyzji liczb w dlaczego duże liczby tracą precyzję.
4. Zera wiodące w stringach są bezpieczne na poziomie tekstu CSV. "phone": "00123" zostaje 00123 w CSV, bo to string od początku do końca i nic go nie drasnie. Drasnie zera dopiero późniejszy krok, gdy Excel traktuje komórkę jako liczbę — zobacz cztery sposoby konwersji JSON do Excela.
5. Pola z przecinkami, cudzysłowami lub znakami nowej linii. papaparse trzyma się RFC 4180: ująć pole w cudzysłów, podwoić wewnętrzne cudzysłowy. "He said \"hi\", ok" wychodzi jako "He said ""hi"", ok" i przechodzi round-trip bez szwanku. Ręcznie pisane serializatory omijające bibliotekę prawie zawsze zawodzą dokładnie tu.
Round-trip: co zmienia przełącznik wnioskowania
CSV → JSON musi zgadywać typ każdej komórki. Nasz konwerter domyślnie wyłącza wnioskowanie typów i zwraca wszystko jako stringi:
[{ "id": "9007199254740992", "phone": "00123" }]
Z włączonym typeInference id staje się liczbą (precyzja już przepadła), a phone pozostaje stringiem — reguły wnioskowania wykluczają formy z zerem wiodącym jak 010, które nie są legalnymi literałami liczbowymi, więc string przeżywa przez szczęście.
Zapamiętaj tę asymetrię: wyłączone wnioskowanie kosztuje Cię najwyżej typy; włączone może kosztować dane. Kolumny, które potrzebują liczb, konwertuje się jawnymi definicjami kolumn, a nie globalnym wnioskowaniem. Implementacja rowsToRecords z naszego wcześniejszego artykułu JSON, CSV i dane tabelaryczne to dokładnie ta zasada w kodzie.
Wybierz strategię według odbiorcy: tabela decyzyjna
| Kto dostaje CSV | Strategia nagłówka | Obsługa tablic | Typy |
|---|---|---|---|
| Kolega, który otworzy raz | Unia kluczy | JSON w komórce | Bez wnioskowania |
| Excel / raportowanie | Unia kluczy | JSON w komórce, albo kolumny indeksowane przy stałej długości | Kolumny-identyfikatory wymuszone jako tekst |
| Import do bazy danych | Jawna lista kolumn | Osobne tabele master/detale, albo rozwinięcie | Jawne definicje kolumn |
| BI / agregacja | Jawna lista kolumn | Rozwinięte wiersze, granularność = najmniejsza jednostka analizy | Jawne definicje kolumn |
| Zautomatyzowany pipeline | Jawna lista kolumn | JSON w komórce, rozwinięcie u odbiorcy | Wnioskowanie wyłączone, konwersja kolumna po kolumnie |
Wspólna zasada: im bliżej człowieka, tym więcej wolno rozluźnić; im bliżej maszyny, tym więcej musi być jawnego.
Checklista do skopiowania
- [ ] Nagłówek pochodzi z pełnej unii kluczy czy z jawnej listy? Jeśli nie — potrafisz udowodnić, że żaden rekord nie stracił pola?
- [ ] Klucze mogą zawierać kropki lub ukośniki? Czy sprawdzono kolizję z separatorem spłaszczania?
- [ ] Strategia tablic jest udokumentowana? Zachowanie pustych tablic jest rozstrzygnięte?
- [ ] Dla
null, braku i pustego stringa: co czyta odbiorca w każdym przypadku? Wybierz regułę i zamroź ją. - [ ] Kolumny-identyfikatory (ID, numery zamówień, telefony) podróżują jako stringi od początku do końca?
- [ ] Po rozwinięciu liczba wierszy równa się sumie długości tablic? Sprawdzone losowo?
- [ ] Pola przykładowe z przecinkami, cudzysłowami i nowymi liniami przechodzą round-trip bez szwanku?
- [ ] Globalne wnioskowanie typów jest wyłączone przy reimporcie, z jawnymi definicjami dla kolumn liczbowych?
Dopiero gdy wszystkie osiem ma jasną odpowiedź, ten CSV jest „poprawny" — a nie tylko „pomyślnie skonwertowany".
FAQ
Dlaczego konwerter nie spłaszcza zagnieżdżonych obiektów do kolumn domyślnie?
Spłaszczanie wyprowadza liczbę kolumn spod kontroli: głębokie zagnieżdżenie produkuje nazwy jak user.address.geo.lat, a w połączeniu z unią kluczy jeden rekord o innej formie dodaje cały stos kolumn. Stringi JSON w komórkach zachowują wierność i stabilną liczbę kolumn; spłaszczaj jawnie, gdy potrzebujesz.
Co zrobić, gdy nagłówek zawiera zduplikowane nazwy kolumn?
Kolizja dwóch różnych kluczy po spłaszczeniu (jak a.b kontra zagnieżdżone a→b) musi zostać rozwiązana po stronie źródła: zmień separator albo przejdź na JSON Pointer. Dla duplikatów wewnątrz tego samego obiektu konwertery zwykle zostawiają ostatnią wartość; duplikaty w nagłówku zależą od implementacji — unparse papaparse nie deduplikuje, a Excel przy otwieraniu cicho scala albo wyrzuca duplikaty. Dlatego jest to w checkliście.
Czy zduplikowane pola rodzica po rozwinięciu to bug?
Nie — to projekt. Rozwinięty wiersz to wiersz szczegółów, a wiersz szczegółów musi dać się doczepić do rekordu rodzica po kluczu. Agreguj po stronie odbiorcy po kluczu rodzica; jeśli rozmiar niepokoi, wyeksportuj plik szczegółów i osobną tabelę master.
Jak sprawdzić, że moja konwersja nie straciła żadnego pola?
Przeprowadź audyt kluczy: policz unię wszystkich kluczy źródłowego JSON, potem zbiór nagłówków kolumn CSV, i zrób różnicę. Niepusta różnica oznacza stracone pola. W pipeline'ach wsadowych zakoduj to jako asercję w testach: każda zmiana konwertera, która zacznie gubić pola, automatycznie upadnie.