Konwersja JSON do i z CSV oraz danych tabelarycznych
Podsumowanie: CSV jest dwuwymiarowy, JSON ma kształt drzewa. Trudna część konwersji to nie kod, ale ustalenie z góry, jak spłaszczać tablice i jak zachować typy.

Najważniejsze: spłaszczaj kluczami z kropkami i tablicami serializowanymi do JSON, gdy eksport ma czytać człowiek; rozdzielaj na tabelę nadrzędną i powiązane, gdy wczytujesz do bazy; rozwijaj tablice w dodatkowe wiersze tylko dla tabel przestawnych; przy przywracaniu konwertuj według jawnych definicji kolumn, a nie globalnego wnioskowania.
Spłaszczenie drzewa do tabeli zawsze gubi hierarchię; przywrócenie tabeli do drzewa zawsze zgaduje typy. W obu kierunkach reguły trzeba ustalić z góry.
1. Co CSV naprawdę określa
RFC 4180 liczy tylko kilka stron i określa mniej, niż wielu osobom się wydaje:
- Każdy rekord zajmuje jeden wiersz, a pola są rozdzielane przecinkami;
- Terminatorem wiersza jest CRLF, choć w praktyce akceptowany jest też
\n; - Pole zawierające przecinek, cudzysłów lub nową linię musi być ujęte w podwójne cudzysłowy;
- Podwójne cudzysłowy wewnątrz pola zapisuje się jako dwa cudzysłowy
""; - Pierwszy wiersz może być nagłówkiem;
- Nie określa kodowania znaków ani typów.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"
Zwróć uwagę na regułę 3: pola zawierające nowe linie są legalne, więc naiwny parser, który dzieli po \n, a potem po ,, z pewnością zawiedzie.
2. JSON → CSV: najpierw zdecyduj o strukturze
Istnieją tylko trzy strategie spłaszczenia struktury drzewa do tabeli dwuwymiarowej:
Strategia 1: spłaszczenie pól (najczęstsza)
{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
- Łącz klucze zagnieżdżonych obiektów kropką
.; - Tablic nie da się wyrazić jako kolumn, więc serializuj je
JSON.stringifyprosto do komórki (co nadal jest legalnym polem CSV); - Wada: arkusze nie potrafią filtrować tablicy, więc wymagane jest ponowne parsowanie.
Strategia 2: rozwinięcie tablicy w wiele wierszy
id,user.name,tag
1,张三,a
1,张三,b
Pasuje do relacji jeden-do-wielu, w których odbiorcy niższego szczebla agregują per element. Wadą są zduplikowane pola nadrzędne i rosnąca liczba wierszy.
Strategia 3: podział na wiele tabel
Tabela główna plus powiązane tabele połączone identyfikatorem — najczystsze podejście, ale wykracza poza „pojedynczy eksport” i wygląda bardziej jak modelowanie danych.
Którą wybrać? Strategię 1 dla eksportów przeznaczonych dla ludzi, strategię 3 do wczytania do bazy, a strategię 2 do tworzenia tabel przestawnych.
3. CSV → JSON: jak zgadywane są typy
CSV nie ma typów, więc każdy etap przywracania jest zgadywaniem:
| Treść komórki | Możliwe znaczenie | Ryzyko bezpośredniej konwersji na liczbę |
|---|---|---|
00123 |
Identyfikator pracownika, kod pocztowy | Utrata zer wiodących → 123 |
1.10 |
Numer wersji, kwota | Staje się 1.1 |
1e5 |
Identyfikator lub notacja naukowa | Staje się 100000 |
2026-10-30 |
Data lub łańcuch | Niejednoznaczność strefy czasowej i formatu |
13800138000 |
Numer telefonu | Wciąż bezpieczny przy 11 cyfrach, ale niektóre identyfikatory tracą precyzję |
"" |
Pusty łańcuch lub null | Niejednoznaczność semantyczna |
Sugerowane domyślne ustawienie: traktuj wszystko jako łańcuch i pozwól schematowi albo jawnej definicji kolumn zdecydować, które kolumny stają się liczbami lub wartościami logicznymi. „Inteligentne wnioskowanie” cieszy przy eksporcie i jest źródłem incydentów przy imporcie.
type ColumnType = 'string' | 'number' | 'boolean' | 'json';
/**
* Converts CSV rows into objects according to the column definitions.
* @param rows The raw row array; the first row is the header
* @param columnTypes Mapping from column name to type
* @returns The converted array of objects
*/
function rowsToRecords(
rows: string[][],
columnTypes: Record<string, ColumnType>,
): Array<Record<string, unknown>> {
const [headerRow, ...dataRows] = rows;
return dataRows.map((dataRow) => {
const record: Record<string, unknown> = {};
headerRow.forEach((columnName, columnIndex) => {
const rawValue = dataRow[columnIndex] ?? '';
const columnType = columnTypes[columnName] ?? 'string';
if (columnType === 'number') {
const numericValue = Number(rawValue);
record[columnName] = Number.isFinite(numericValue) ? numericValue : null;
return;
}
if (columnType === 'boolean') {
record[columnName] = rawValue === 'true' || rawValue === '1';
return;
}
if (columnType === 'json') {
try {
record[columnName] = JSON.parse(rawValue);
} catch {
record[columnName] = rawValue;
}
return;
}
record[columnName] = rawValue;
});
return record;
});
}
4. Trzy pułapki związane z Excelem
4.1 Kodowanie
Gdy Excel otwiera CSV UTF-8 bez BOM, chiński tekst zamienia się w krzaczki. Dodanie BOM (\uFEFF) przy eksporcie jest najtańszą poprawką zgodności:
const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;
4.2 Długie liczby zamieniające się w notację naukową
18-cyfrowy numer dowodu lub 19-cyfrowy numer zamówienia pojawia się w Excelu jako 1.23457E+17, a sama wartość została już obcięta.
Poprzedzenie pola tabulatorem lub opakowanie w formułę (="1234...") zachowuje formatowanie tekstowe, ale zamienia komórkę w formułę.
Bezpieczniejszym podejściem jest poinformowanie użytkowników, aby „użyli funkcji importu i wskazali kolumnę jako tekst”, albo udostępnienie .xlsx obok eksportu.
4.3 Separatory zależne od ustawień regionalnych
Niektóre ustawienia europejskie domyślnie używają średnika ; jako separatora. Przy dystrybucji między regionami albo ustal CSV na przecinki i powiedz to wprost, albo udostępnij wersję TSV (rozdzielaną tabulatorami) — TSV jest bardziej tolerancyjny regionalnie.
5. Obsługa dużych plików
Nie wczytuj do pamięci naraz CSV z ponad setką tysięcy wierszy:
import { createReadStream } from 'node:fs';
import { parse } from 'csv-parse';
const parser = createReadStream('orders.csv').pipe(
parse({ columns: true, skipEmptyLines: true, bom: true }),
);
for await (const record of parser) {
// Process one record at a time; memory usage is independent of file size
}
Kierunek eksportu działa tak samo: generuj i zapisuj do strumienia na bieżąco, zamiast sklejać jeden wielki łańcuch.
6. Samokontrola spójności w obie strony
Pełny obieg CSV (JSON → CSV → JSON) prawie nigdy nie jest dokładnie identyczny, ale możesz zdefiniować, które straty są akceptowalne:
| Kontrola | Oczekiwanie |
|---|---|
| Liczba pól | Identyczna |
| Wartości łańcuchowe | Identyczne (w tym zera wiodące) |
| Typy numeryczne | Określone przez definicję kolumny, a nie przez wnioskowanie |
| Zagnieżdżone obiekty | Odzyskiwalne przez ścieżki z kropkami |
| Tablice | Widoczne po serializacji do łańcucha JSON |
| Puste wartości | Znormalizowane do null lub pustego łańcucha — wybierz dokładnie jedną opcję |
Umieszczenie tej tabeli w testach jest znacznie skuteczniejsze niż ustalenia słowne.
7. Lista kontrolna
- [ ] Semantyka „jeden wiersz to jeden rekord” jest określona jawnie i widoczna w nazwie pliku eksportu;
- [ ] Każda kolumna, która może utracić precyzję, jest eksportowana jako łańcuch;
- [ ] Chińskie pliki CSV zawierają BOM albo kodowanie jest określone jawnie;
- [ ] Pola zawierające przecinki, cudzysłowy lub nowe linie są poprawnie escape'owane (użyj biblioteki, nie sklejaj łańcuchów ręcznie);
- [ ] Duże pliki przechodzą przez strumieniowy odczyt i zapis;
- [ ] Import konwertuje typy według definicji kolumn, nigdy przez globalne inteligentne wnioskowanie.
FAQ
Jak wybrać między spłaszczeniem, rozwijaniem wierszy a podziałem tabel?
Spłaszczaj przy eksportach dla ludzi: łącz zagnieżdżone klucze kropką i serializuj tablice do komórki przez JSON.stringify, kosztem braku filtrowania tych tablic w arkuszu. Podziel na tabelę nadrzędną plus powiązane, gdy celem jest baza danych. Rozwiń tablice w dodatkowe wiersze, gdy odbiorcy niższego szczebla agregują per element, akceptując zduplikowane pola nadrzędne.
Dlaczego import CSV nie powinien wnioskować typów automatycznie?
RFC 4180 określa separatory i ucieczkę, ale nie typy, więc każdy etap przywracania jest zgadywaniem: 00123 traci zera wiodące, 1.10 staje się 1.1, a 1e5 staje się 100000. Traktuj wszystko domyślnie jako łańcuch i pozwól schematowi lub jawnej definicji kolumn zdecydować, które kolumny stają się liczbami lub wartościami logicznymi.
Czy pełny obieg CSV powinien odtwarzać oryginalny JSON dokładnie?
Prawie nigdy. Zdefiniuj zamiast tego akceptowalne straty: identyczna liczba pól, identyczne wartości łańcuchowe z zerami wiodącymi, typy numeryczne ustalone przez definicję kolumny, zagnieżdżone obiekty odzyskiwalne przez ścieżki z kropkami, tablice widoczne po serializacji oraz puste wartości znormalizowane do null lub pustego łańcucha — wybierz dokładnie jedną opcję.