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.

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:
- Encoding adalah UTF-8, tanpa BOM;
- Setiap baris memuat satu nilai JSON lengkap dan valid;
- Baris dipisahkan oleh
\n; baris terakhir boleh atau tidak diakhiri baris baru; - Tidak boleh ada baris baru yang tidak di-escape di dalam satu baris — baris baru di dalam string harus ditulis sebagai
\n; - 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:
- Output
JSON.stringifytidak mengandung baris baru (kecuali\ndalam string di-escape menjadi dua karakter), sehingga satu baris selalu tepat satu record; - Ketika beberapa proses menambahkan secara bersamaan, satu
writeyang 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
.jsonldengan rasio konsisten tinggi (nama field berulang), sehingga.jsonl.gzdisarankan; - 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
.jsonldengan "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 (
.jsonlatau.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.