JSON Toolkit

JSON Lines dan NDJSON: Data Streaming

Intinya: Satu nilai JSON per baris memberi Anda kemampuan menambahkan, streaming, dan kerusakan yang terisolasi pada satu baris; harganya adalah tidak adanya struktur keseluruhan, sehingga validasi harus dilakukan baris demi baris.

Menengah±10 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-30
Sampul artikel JSON Lines dan NDJSON data streaming

Intinya: gunakan array JSON ketika Anda memproses seluruh batch sekaligus, dan JSON Lines ketika Anda membutuhkan penambahan streaming serta isolasi baris buruk; jika Anda masih perlu memvalidasi struktur keseluruhan, jangan beralih ke format per baris.

Begitu data melampaui memori, atau terus datang secara berkelanjutan, struktur "satu array besar" menjadi musuh.

1. Aturan format

JSON Lines (.jsonl) dan NDJSON (.ndjson) praktis sinonim, dan aturannya singkat dalam kedua kasus:

  1. Encoding adalah UTF-8, tanpa BOM;
  2. Setiap baris memuat satu nilai JSON lengkap dan valid;
  3. Baris dipisahkan oleh \n; baris terakhir boleh atau tidak diakhiri baris baru;
  4. Tidak boleh ada baris baru yang tidak di-escape di dalam satu baris — baris baru di dalam string harus ditulis sebagai \n;
  5. Baris kosong biasanya diabaikan, tetapi implementasi ketat sebaiknya memperlakukannya sebagai tidak valid.
{"time":"2026-10-30T10:41:45+08:00","level":"info","message":"boot"}
{"time":"2026-10-30T10:41:46+08:00","level":"warn","message":"slow query","costMs":812}
{"time":"2026-10-30T10:41:47+08:00","level":"error","message":"timeout","traceId":"a1b2"}

Perbedaan halus antara keduanya: situs web JSON Lines menekankan "satu nilai per baris, dan nilainya boleh JSON apa pun (termasuk skalar)", sementara spesifikasi NDJSON menambahkan aturan implementasi yang lebih ketat seperti "tidak ada baris kosong dan tidak ada skalar tingkat atas". Dalam rekayasa, mengikuti kumpulan aturan yang lebih ketat menjauhkan Anda dari masalah.

2. Perbandingan dengan array JSON

Dimensi Array JSON JSON Lines
Menambahkan satu record Menulis ulang seluruh file (mengubah ] penutup) Cukup menambahkan satu baris
Pembacaan streaming Memerlukan parser streaming yang melacak kedalaman kurung Dipecah per baris
Satu entri rusak Seluruh file menjadi tidak dapat di-parse Hanya baris itu yang terpengaruh
Validasi seluruh file Validasi semuanya dalam satu lintasan Hanya baris demi baris
Kompresi Ukuran serupa setelah gzip Sama, dan blok dapat dikompresi secara paralel
Mengekspresikan penyarangan Didukung secara native Setiap baris dapat bersarang di dalamnya; baris tidak saling terkait
Konsumen tipikal Semua alat JSON Sistem log, pipeline big data, dataset pelatihan LLM

Kesimpulannya langsung: apakah data itu "sekumpulan record" atau "satu dokumen" menentukan format mana yang dipakai.

3. Membaca: streaming baris demi baris

Di Node, pendekatan paling sederhana adalah readline:

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

/**
 * Reads a JSON Lines file line by line and invokes a callback for each record.
 * @param filePath Path to the file
 * @param onRecord Handler for a single record
 * @returns Statistics containing the number of successful and failed lines
 */
async function readJsonLines(
  filePath: string,
  onRecord: (record: unknown, lineNumber: number) => void,
): Promise<{ okCount: number; badCount: number }> {
  const lineReader = createInterface({
    input: createReadStream(filePath, { encoding: 'utf8' }),
    crlfDelay: Infinity,
  });

  let okCount = 0;
  let badCount = 0;

  for await (const rawLine of lineReader) {
    const line = rawLine.trim();
    if (!line) {
      continue;
    }
    try {
      onRecord(JSON.parse(line), okCount + badCount + 1);
      okCount += 1;
    } catch {
      badCount += 1;
    }
  }

  return { okCount, badCount };
}

Penggunaan memori kode ini hanya bergantung pada ukuran satu baris, bukan pada ukuran total file — itulah nilai terbesar dari format per baris.

3.1 Ketika Anda hanya membutuhkan beberapa field, jangan JSON.parse dulu

Jika setiap record memiliki 50 field dan Anda hanya membutuhkan 3, JSON.parse membangun objek untuk semua 50 field tersebut. Parser streaming (stream-json, clarinet) dapat mengekstrak hanya field target, dengan biaya kompleksitas implementasi yang lebih tinggi. Aturan praktisnya: ketika satu record di bawah beberapa ratus KB, memanggil JSON.parse secara langsung biasanya masih lebih menguntungkan.

3.2 Teks Tionghoa dan BOM

File .jsonl yang dihasilkan di Windows sering membawa BOM, dan JSON.parse pada baris pertama dipastikan gagal. Hapus BOM sebelum membaca, atau periksa tiga byte pertama saat men-streaming chunk pertama.

4. Menulis: penambahan dan atomisitas

import { appendFile } from 'node:fs/promises';

/**
 * Appends a single JSON Lines record.
 * @param filePath Target file
 * @param record The record object to write
 * @returns A promise that resolves when the write completes
 */
async function appendRecord(filePath: string, record: unknown): Promise<void> {
  const line = `${JSON.stringify(record)}\n`;
  await appendFile(filePath, line, { encoding: 'utf8' });
}

Dua hal yang perlu diperhatikan:

  1. Output JSON.stringify tidak mengandung baris baru (kecuali \n dalam string di-escape menjadi dua karakter), sehingga satu baris selalu tepat satu record;
  2. Ketika beberapa proses menambahkan secara bersamaan, satu write yang lebih kecil dari PIPE_BUF (biasanya 4096 byte di Linux) bersifat atomik; apa pun yang lebih besar tidak dijamin. Untuk skenario konkurensi tinggi, beralih ke "satu file per proses, digabungkan setelah di-flush ke disk", atau gunakan penyimpanan dengan transaksi.

5. Menangani baris buruk

Ini adalah keputusan yang dipaksa oleh format per baris untuk Anda ambil lebih awal:

Strategi Cocok untuk Biaya
Lewati dan hitung Data yang menoleransi celah, seperti log dan event analitik Data hilang secara diam-diam, jadi peringatan wajib
Gagal segera Data yang tidak boleh hilang, seperti penagihan dan faktur Satu baris buruk menghambat seluruh batch
Arahkan ke file dead-letter Kasus yang perlu diisi ulang nanti Penyimpanan tambahan dan proses pemutaran ulang

Apa pun pilihan Anda, catat nomor baris dan cuplikan konten asli, jika tidak Anda tidak dapat melokasikan masalah setelahnya. Nomor baris tidak ada dalam pesan error JSON.parse, jadi Anda harus memeliharanya sendiri.

6. Kompresi dan sharding

  • gzip mengompresi .jsonl dengan rasio konsisten tinggi (nama field berulang), sehingga .jsonl.gz disarankan;
  • gzip mengompresi dalam blok dan secara native mendukung dekompresi streaming, sehingga tidak perlu mendekompresi seluruh file;
  • Pecah file besar menjadi potongan sekitar 1GB, dengan nomor urut dalam nama, agar pemrosesan paralel dan transfer yang dapat dilanjutkan lebih mudah;
  • Jangan pernah memproses awal .jsonl dengan "menggabungkannya menjadi array" — itu membuang semua keunggulannya.

7. Kompromi terhadap format big data lainnya

Format Struktur Schema disertakan Cocok untuk
JSON Lines Independen per baris Tidak (butuh kontrak eksternal) Log, impor dan ekspor, data pelatihan
CSV Tabular Tidak Laporan, pertukaran Excel
Parquet Kolumnar Ya Kueri analitik, dataset offline sangat besar
Avro Berbasis baris Ya Pesan Kafka, evolusi schema

Keunggulan JSON Lines adalah bahasa apa pun dan alat apa pun dapat membacanya dan menulisnya segera; kelemahannya adalah kurangnya schema dan kompresi kolumnar. Begitu data tidak lagi muat di satu mesin dan Anda membutuhkan kueri berorientasi kolom secara sering, saatnya mempertimbangkan Parquet.

8. Checklist

  • [ ] Ekstensi file konsisten (.jsonl atau .ndjson) dan tidak pernah dicampur;
  • [ ] Penulis menjamin "satu record per baris", dengan baris baru di dalam string sudah di-escape;
  • [ ] Pembaca menghapus BOM, melewati baris kosong, dan memelihara nomor baris;
  • [ ] Strategi baris buruk didefinisikan dan dipantau secara eksplisit (baris buruk / total baris);
  • [ ] File besar dipotong berdasarkan ukuran dan dipasangkan dengan gzip dan pembacaan paralel;
  • [ ] Di tempat kontrak field ada, schema tambahan disediakan dan divalidasi baris demi baris.

FAQ

Bagaimana memilih antara array JSON dan JSON Lines?

Lihat apakah data itu sekumpulan record atau satu dokumen. Pilih JSON Lines ketika record ditambahkan dan dibaca sebagai aliran: menambahkan berarti menulis satu baris di akhir, dan membaca hanya membutuhkan pemisahan per baris. Pertahankan array JSON ketika seluruh struktur harus divalidasi dalam satu lintasan, karena array JSON memvalidasi semuanya sekaligus sementara JSON Lines hanya dapat divalidasi baris demi baris.

Mengapa satu baris rusak tidak merusak seluruh file?

Setiap baris dalam JSON Lines adalah nilai JSON yang independen dan lengkap, dan parsing secara alami dilakukan baris demi baris, sehingga data rusak hanya memengaruhi baris itu. Dalam array JSON, satu error sintaks membuat seluruh file tidak dapat di-parse, itulah sebabnya data per baris cocok untuk log dan event analitik.

Haruskah saya mengonversi JSON Lines menjadi array JSON?

Bisa: baca baris demi baris, panggil JSON.parse pada setiap record, dan kumpulkan ke dalam array. Melakukannya mengharuskan seluruh konten muat di memori dan melepaskan pembacaan streaming serta penambahan satu baris. Memproses awal .jsonl menjadi array membuang semua keunggulan format tersebut, jadi itu hanya masuk akal untuk pengiriman sekali pakai di mana data muat di memori.