JSON Toolkit

Mengonversi JSON ke dan dari CSV serta Data Tabular

Intinya: CSV bersifat dua dimensi, JSON berbentuk pohon. Bagian tersulit dari konversi bukanlah kode — melainkan memutuskan di awal bagaimana array diratakan dan bagaimana tipe dipertahankan.

Pemula±10 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-30
Sampul untuk konversi JSON ↔ CSV dan data tabular

Intinya: ratakan dengan kunci bertitik dan array yang di-JSON-stringify saat orang akan membaca ekspornya, pecah menjadi tabel induk plus tabel terkait saat memuat basis data, dan perluas array menjadi baris tambahan hanya untuk pivot; saat memulihkan, konversi berdasarkan definisi kolom eksplisit alih-alih inferensi global.

Meratakan pohon menjadi tabel selalu menghilangkan hierarki; memulihkan tabel menjadi pohon selalu menebak tipe. Kedua arah membutuhkan aturannya dinyatakan lebih dulu.

1. Apa yang sebenarnya ditetapkan CSV

RFC 4180 hanya beberapa halaman dan menetapkan lebih sedikit daripada yang dibayangkan banyak orang:

  1. Setiap record menempati satu baris, dengan field dipisahkan koma;
  2. Terminator baris adalah CRLF, meskipun dalam praktik \n juga diterima;
  3. Field yang berisi koma, kutip ganda, atau baris baru harus dibungkus kutip ganda;
  4. Kutip ganda di dalam field ditulis sebagai dua kutip ganda "";
  5. Baris pertama boleh berupa header;
  6. Tidak ada encoding karakter yang ditetapkan, dan tipe juga tidak.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

Perhatikan aturan 3: field yang berisi baris baru legal, sehingga parser naif yang memisahkan pada \n lalu pada , dipastikan salah.

2. JSON → CSV: tentukan strukturnya lebih dulu

Hanya ada tiga strategi untuk meratakan struktur pohon menjadi tabel dua dimensi:

Strategi 1: ratakan field (yang paling umum)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • Gabungkan kunci object bersarang dengan .;
  • Array tidak dapat dinyatakan sebagai kolom, jadi JSON.stringify langsung ke dalam sel (yang tetap merupakan field CSV legal);
  • Kekurangan: alat spreadsheet tidak dapat memfilter array, sehingga diperlukan parse kedua.

Strategi 2: perluas array menjadi beberapa baris

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

Ini cocok untuk relasi satu-ke-banyak di mana konsumen hilir mengagregasi per elemen. Kekurangannya adalah field induk terduplikasi dan jumlah baris membengkak.

Strategi 3: pecah menjadi beberapa tabel

Tabel utama plus tabel terkait yang digabungkan oleh id — pendekatan paling bersih, tetapi melampaui "satu ekspor" dan lebih terlihat seperti pemodelan data.

Mana yang dipilih? Gunakan strategi 1 untuk ekspor yang ditujukan bagi manusia, strategi 3 untuk memuat ke basis data, dan strategi 2 untuk mem-pivot data.

3. CSV → JSON: bagaimana tipe ditebak

CSV tidak punya tipe, sehingga setiap langkah pemulihan adalah tebakan:

Isi sel Kemungkinan arti Risiko mengonversi langsung ke number
00123 ID karyawan, kode pos Nol di depan hilang → 123
1.10 Nomor versi, jumlah Menjadi 1.1
1e5 Pengenal atau notasi ilmiah Menjadi 100000
2026-10-30 Tanggal atau string Ambiguitas zona waktu dan format
13800138000 Nomor telepon Masih aman pada 11 digit, tetapi sebagian ID memang kehilangan presisi
"" String kosong atau null Ambiguitas semantik

Default yang disarankan: perlakukan semuanya sebagai string, dan biarkan schema atau definisi kolom eksplisit memutuskan kolom mana yang menjadi number atau boolean. "Inferensi pintar" menyenangkan saat ekspor dan menjadi sumber insiden saat impor.

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. Tiga jebakan seputar Excel

4.1 Encoding

Saat Excel membuka CSV UTF-8 tanpa BOM, teks Tionghoa menjadi mojibake. Menambahkan BOM (\uFEFF) saat ekspor adalah perbaikan kompatibilitas termurah:

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

4.2 Angka panjang berubah menjadi notasi ilmiah

Nomor identitas nasional 18 digit atau nomor order 19 digit tampil di Excel sebagai 1.23457E+17, dan nilainya sendiri sudah terpotong. Memberi prefiks tab pada field atau membungkusnya sebagai formula (="1234...") mempertahankan format teks, tetapi mengubah sel menjadi formula. Pendekatan yang lebih aman adalah memberi tahu pengguna untuk "menggunakan fitur impor dan menentukan kolom sebagai teks", atau menawarkan .xlsx bersama ekspornya.

4.3 Pemisah dipengaruhi locale

Sebagian locale Eropa memakai titik koma ; sebagai pemisah default. Saat mendistribusikan lintas wilayah, entah tetapkan CSV pada koma dan nyatakan begitu, atau tawarkan versi TSV (tab-separated) — TSV lebih toleran terhadap locale.

5. Menangani file besar

Jangan memuat CSV dengan lebih dari seratus ribu baris ke memori sekaligus:

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
}

Arah ekspor bekerja dengan cara yang sama: hasilkan dan tulis ke stream sambil berjalan, bukan menyambung satu string raksasa.

6. Pemeriksaan mandiri konsistensi round-trip

Round trip CSV (JSON → CSV → JSON) hampir tidak pernah benar-benar identik, tetapi Anda dapat mendefinisikan kehilangan mana yang dapat diterima:

Pemeriksaan Harapan
Jumlah field Identik
Nilai string Identik (termasuk nol di depan)
Tipe numerik Ditentukan oleh definisi kolom, bukan inferensi
Object bersarang Dapat dipulihkan melalui jalur bertitik
Array Terlihat setelah serialisasi ke string JSON
Nilai kosong Dinormalisasi menjadi null atau string kosong — pilih tepat satu

Menaruh tabel ini ke dalam tes jauh lebih efektif daripada kesepakatan lisan.

7. Daftar periksa

  • [ ] Semantik "satu baris adalah satu record" dinyatakan eksplisit dan tercermin pada nama file ekspor;
  • [ ] Setiap kolom yang mungkin kehilangan presisi diekspor sebagai string;
  • [ ] CSV Tionghoa menyertakan BOM, atau encoding-nya dinyatakan eksplisit;
  • [ ] Field yang berisi koma, kutip, atau baris baru di-escape dengan benar (gunakan pustaka, jangan menyambung string sendiri);
  • [ ] File besar melalui pembacaan dan penulisan streaming;
  • [ ] Impor mengonversi tipe per definisi kolom, tidak pernah melalui inferensi pintar global.

FAQ

Bagaimana cara memilih antara meratakan, memperluas baris, dan memecah tabel?

Ratalkan untuk ekspor yang ditujukan bagi manusia: gabungkan kunci bersarang dengan . dan JSON.stringify array ke dalam sel, dengan konsekuensi tidak dapat memfilter array tersebut di spreadsheet. Pecah menjadi tabel induk plus tabel terkait saat targetnya basis data. Perluas array menjadi baris tambahan saat konsumen hilir mengagregasi per elemen, dengan menerima field induk terduplikasi.

Mengapa impor CSV tidak boleh menebak tipe secara otomatis?

RFC 4180 menetapkan pemisah dan escaping tetapi bukan tipe, sehingga setiap langkah pemulihan adalah tebakan: 00123 kehilangan nol di depannya, 1.10 menjadi 1.1, dan 1e5 menjadi 100000. Perlakukan semuanya sebagai string secara default dan biarkan schema atau definisi kolom eksplisit memutuskan kolom mana yang menjadi number atau boolean.

Haruskah saya mengharapkan round trip CSV mereproduksi JSON asli secara persis?

Hampir tidak pernah. Definisi kehilangan yang dapat diterima: jumlah field identik, nilai string identik termasuk nol di depan, tipe numerik ditentukan oleh definisi kolom, object bersarang dapat dipulihkan melalui jalur bertitik, array terlihat setelah serialisasi, dan nilai kosong dinormalisasi menjadi null atau string kosong — pilih tepat satu.