JSON Toolkit

Převod JSON na CSV a tabulková data a zpět

Shrnutí: CSV je dvourozměrné, JSON má tvar stromu. Obtížná část převodu není kód – je to rozhodnutí předem, jak se zplošťují pole a jak se zachovávají typy.

Začátečník~10 min čteníPublikováno 2026-10-30Aktualizováno 2026-10-30
Titulní obrázek článku Převod JSON ↔ CSV a tabulkových dat

Stručně: zplošťujte pomocí tečkových klíčů a polí serializovaných do JSON řetězce, když export budou číst lidé; rozdělte data na mateřskou tabulku a související tabulky, když je načítáte do databáze; pole rozšiřte na další řádky jen pro pivoty; při obnově převádějte podle explicitních definic sloupců, nikoli globálním odvozováním.

Zploštění stromu do tabulky vždy ztratí hierarchii; obnova tabulky do stromu vždy hádá typy. Oba směry potřebují pravidla stanovená předem.

1. Co CSV vlastně specifikuje

RFC 4180 má jen několik stránek a specifikuje méně, než mnozí předpokládají:

  1. Každý záznam zabírá jeden řádek, pole jsou oddělena čárkami;
  2. Ukončovač řádku je CRLF, i když v praxi se přijímá i \n;
  3. Pole obsahující čárku, dvojitou uvozovku nebo zalomení řádku musí být uzavřeno v dvojitých uvozovkách;
  4. Dvojité uvozovky uvnitř pole se zapisují jako dvě dvojité uvozovky "";
  5. První řádek může být hlavička;
  6. Není specifikováno kódování znaků a stejně tak nejsou specifikovány typy.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

Všimněte si pravidla 3: pole obsahující zalomení řádku jsou legální, takže naivní parser, který nejprve rozdělí podle \n a poté podle ,, bude zaručeně špatně.

2. JSON → CSV: nejprve rozhodněte strukturu

Existují pouze tři strategie pro zploštění stromové struktury do dvourozměrné tabulky:

Strategie 1: zploštění polí (nejčastější)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • Klíče vnořených objektů spojte pomocí .;
  • Pole nelze vyjádřit jako sloupce, takže je rovnou JSON.stringify do buňky (což je stále legální pole CSV);
  • Nevýhoda: tabulkové nástroje nemohou pole filtrovat, takže je nutné druhé parsování.

Strategie 2: rozšíření pole na více řádků

id,user.name,tag
1,张三,a
1,张三,b

Hodí se pro vztahy jedna k mnoha, kde příjemci agregují po prvcích. Nevýhodou je duplikace polí rodiče a nafouknutý počet řádků.

Strategie 3: rozdělení do více tabulek

Hlavní tabulka plus související tabulky spojené přes id – nejčistší přístup, ale přesahuje „jeden export“ a vypadá spíš jako datové modelování.

Kterou zvolit? Strategii 1 pro exporty určené lidem, strategii 3 pro načtení do databáze a strategii 2 pro pivotování dat.

3. CSV → JSON: jak se hádají typy

CSV nemá typy, takže každý krok obnovy je hádání:

Obsah buňky Možný význam Riziko přímého převodu na číslo
00123 ID zaměstnance, PSČ Úvodní nuly se ztratí → 123
1.10 Číslo verze, částka Změní se na 1.1
1e5 Identifikátor nebo vědecký zápis Změní se na 100000
2026-10-30 Datum nebo řetězec Nejistota v časovém pásmu a formátu
13800138000 Telefonní číslo S 11 číslicemi ještě bezpečné, ale některá ID přesnost ztratí
"" Prázdný řetězec nebo null Sémantická nejistota

Doporučený výchozí stav: vše považujte za řetězec a nechte schéma nebo explicitní definici sloupců rozhodnout, které sloupce se stanou čísly nebo booleany. „Chytré odvozování“ je při exportu okouzlující a při importu zdrojem incidentů.

type ColumnType = 'string' | 'number' | 'boolean' | 'json';

/**
 * Převede řádky CSV na objekty podle definic sloupců.
 * @param rows Surové pole řádků; první řádek je hlavička
 * @param columnTypes Mapování názvu sloupce na typ
 * @returns Převedené pole objektů
 */
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. Tři úskalí kolem Excelu

4.1 Kódování

Když Excel otevře CSV v UTF-8 bez BOM, čínský text se změní v mojibake. Přidání BOM (\uFEFF) při exportu je nejlevnější oprava kompatibility:

const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;

4.2 Dlouhá čísla se mění na vědecký zápis

Osmnácticiferné rodné číslo nebo devatenácticiferné číslo objednávky se v Excelu zobrazí jako 1.23457E+17 a hodnota samotná už byla zkrácena. Prefixování pole tabulátorem nebo obalení jako vzorec (="1234...") zachová textový formát, ale buňku změní na vzorec. Bezpečnější je říci uživatelům „použijte funkci importu a určete sloupec jako text“, nebo nabídnout k exportu i .xlsx.

4.3 Oddělovače ovlivněné národním prostředím

Některá evropská národní prostředí používají jako oddělovač ve výchozím nastavení středník ;. Při distribuci napříč regiony buď CSV pevně svažte s čárkou a uveďte to, nebo nabídněte verzi TSV (oddělenou tabulátory) – TSV je k národním prostředím tolerantnější.

5. Zpracování velkých souborů

CSV se stovkami tisíc řádků nenačítejte do paměti najednou:

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) {
  // Zpracovávejte jeden záznam najednou; spotřeba paměti nezávisí na velikosti souboru
}

Směr exportu funguje stejně: generujte a průběžně zapisujte do proudu, místo abyste slepovali jeden obrovský řetězec.

6. Sebekontrola konzistence obousměrného převodu

Obousměrný převod CSV (JSON → CSV → JSON) téměř nikdy není přesně shodný, ale můžete definovat, které ztráty jsou přijatelné:

Kontrola Očekávání
Počet polí Shodný
Hodnoty řetězců Shodné (včetně úvodních nul)
Číselné typy Určené definicí sloupce, nikoli odvozováním
Vnořené objekty Obnovitelné přes tečkované cesty
Pole Viditelná po serializaci do JSON řetězce
Prázdné hodnoty Normalizované na null nebo prázdný řetězec – vyberte přesně jednu variantu

Zařazení této tabulky do testů je mnohem účinnější než slovní dohoda.

7. Kontrolní seznam

  • [ ] Sémantika „jeden řádek je jeden záznam“ je výslovně uvedena a promítnuta do názvu exportovaného souboru;
  • [ ] Každý sloupec, který může ztratit přesnost, je exportován jako řetězec;
  • [ ] Čínské CSV obsahuje BOM, nebo je kódování výslovně uvedeno;
  • [ ] Pole obsahující čárky, uvozovky nebo zalomení řádku jsou správně escapována (použijte knihovnu, neskládejte řetězce ručně);
  • [ ] Velké soubory procházejí streamovaným čtením a zápisem;
  • [ ] Import převádí typy podle definice sloupce, nikdy globálním chytrým odvozováním.

Časté dotazy

Jak se rozhodnout mezi zploštěním, rozšířením řádků a rozdělením tabulek?

Zplošťujte pro exporty určené lidem: spojte vnořené klíče pomocí . a pole serializujte pomocí JSON.stringify do buňky, za cenu nemožnosti tato pole v tabulkovém nástroji filtrovat. Rozdělte data na mateřskou tabulku plus související tabulky, když je cílem databáze. Rozšiřte pole na další řádky, když příjemci agregují po prvcích, a smiřte se s duplikovanými poli rodiče.

Proč by import CSV neměl odvozovat typy automaticky?

RFC 4180 specifikuje oddělovače a escaping, ale žádné typy, takže každý krok obnovy je hádání: 00123 ztratí úvodní nuly, 1.10 se změní na 1.1 a 1e5 na 100000. Ve výchozím nastavení považujte vše za řetězec a nechte schéma nebo explicitní definici sloupce rozhodnout, které sloupce se stanou čísly nebo booleany.

Mám očekávat, že obousměrný převod CSV přesně reprodukuje původní JSON?

Téměř nikdy. Definujte místo toho přijatelné ztráty: shodný počet polí, shodné hodnoty řetězců včetně úvodních nul, číselné typy určené definicí sloupce, vnořené objekty obnovitelné přes tečkované cesty, pole viditelná po serializaci a prázdné hodnoty normalizované na null nebo prázdný řetězec – vyberte přesně jednu variantu.