JSONL dalam praktik: membuka, mengonversi, dan memperbaiki baris rusak
Intinya: Memberikan seluruh file .jsonl ke JSON.parse lalu menerima error hampir tidak pernah berarti file rusak — artinya strategi membaca yang salah. Panduan ini mencakup cara standar memeriksa dan membaca JSONL baris demi baris, perintah konversi dua arah dengan array JSON, tabel enam jenis baris rusak, dan cara benar mengonsumsi NDJSON dari respons HTTP streaming.

Intinya: setiap baris file JSONL adalah nilai JSON yang valid dan mandiri, tetapi keseluruhan file bukan satu dokumen JSON. Jika memberikan seluruh file ke JSON.parse atau json.load gagal, sembilan dari sepuluh kali file-nya baik-baik saja — strategi membaca Anda yang keliru.
Satu rekaman per baris, tanpa koma di antara baris, tanpa array pembungkus — JSONL melepas "struktur keseluruhan dokumen" untuk mendapatkan penulisan tambahan, pembacaan streaming, dan isolasi kerusakan per baris. Ketiga keunggulan itu hanya terwujud ketika file diproses baris demi baris, dan ketiganya berubah menjadi error begitu Anda mencoba mengurai seluruh file sekaligus.
1. Tiga nama, satu format: JSONL, NDJSON, JSON Lines
Pemula sering menyangka perbedaan nama berarti perbedaan sintaks. Tidak:
| Nama | Konteks khas | Ekstensi / media type |
|---|---|---|
| JSON Lines | Rekayasa data, dataset ML (pandas, Spark, Hugging Face) | .jsonl |
| NDJSON | API HTTP streaming, data real-time | .ndjson, application/x-ndjson |
| JSONL | Singkatan sehari-hari untuk salah satunya | — |
Alat apa pun yang membaca salah satu pasti membaca yang lain. Satu-satunya kriteria memilih nama adalah konvensi ekosistem hilir Anda: tulis .jsonl saat menyiapkan dataset untuk Hugging Face, atur application/x-ndjson saat menyajikan API streaming. File JSONL yang portabel memenuhi tiga aturan: pengodean UTF-8 (tanpa BOM), satu nilai JSON lengkap per baris, baris dipisahkan \n. Baris baru penutup di akhir file sangat disarankan, dan baris kosong bukan rekaman yang valid. Detail protokol dan alasan desain dibahas dalam ringkasan JSON Lines dan NDJSON kami; artikel ini secara ketat membahas apa yang harus dilakukan ketika file sudah ada di tangan.
2. Pemeriksaan: head dulu, lalu grep, editor paling akhir
Mengklik dua kali file .jsonl berukuran 500 MB untuk membukanya di editor adalah kesalahan paling umum: tiga juta baris teks plus penyorotan sintaks plus buffer undo akan membekukan hampir semua editor — dan JSON tidak bersalah. Mengurai dokumen 45 MB memakan sekitar 208 ms di Node 22; yang membeku adalah perenderan. Format berorientasi baris diciptakan untuk alat berbasis baris, karena rekaman berimpit dengan baris fisik:
# Ukur skalanya dulu: jumlah rekaman dan ukuran file, tanpa membaca isi
wc -l events.jsonl && du -h events.jsonl
# Lihat wujud satu rekaman (pastikan field dan strukturnya)
head -n 1 events.jsonl | jq .
# Temukan rekaman target langsung: alat berbasis baris berfungsi begitu saja — hak istimewa JSONL
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'
Perhatikan perintah terakhir: setiap baris keluar dari tail tetap nilai JSON mandiri, sehingga bisa langsung masuk ke jq tanpa perlu membangun ulang array. Pendekatan ini bekerja identik pada file beberapa gigabyte dengan overhead memori nyaris nol — prinsip yang sama dengan mengapa file JSON besar memperlambat halaman.
3. Tiga pola standar pembacaan baris demi baris
Pembacaan baris demi baris harus menangani tiga hal: melewati baris kosong (terutama baris baru penutup), melaporkan error dengan nomor baris, dan tidak membiarkan satu baris rusak membunuh seluruh pekerjaan. Pola standar di ketiga bahasa berputar pada tiga poin ini.
Python, hanya pustaka standar:
import json
with open("events.jsonl", encoding="utf-8") as f:
for line_number, line in enumerate(f, start=1):
if not line.strip(): # lewati baris kosong
continue
try:
record = json.loads(line) # urai setiap baris secara mandiri
except json.JSONDecodeError as e:
print(f"Baris {line_number} gagal: {e}") # laporkan, jangan berhenti
continue # isolasi baris rusak
handle(record)
Node dengan modul readline bawaan, memori konstan berapa pun ukuran file:
import fs from "node:fs";
import readline from "node:readline";
const input = fs.createReadStream("events.jsonl", "utf8");
const lines = readline.createInterface({ input, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of lines) {
lineNumber += 1;
if (!line.trim()) continue;
try {
handle(JSON.parse(line));
} catch (e) {
console.error(`Baris ${lineNumber}: ${e.message}`);
}
}
Di Go 1.27, encoding/json/v2 menjadi standar bawaan, dekoder streaming per nilai-nya pas sekali dengan JSONL; gunakan gojq jika perlu menanamkan ekspresi jq. Soal pustaka, jangan buru-buru memasang paket: jsonlines di Python melaporkan nomor dan teks mentah setiap baris yang gagal, dan orjsonl membaca serta menulis file terkompresi gzip/bzip2/zstd langsung; paket Node ndjson tidak merilis versi sejak 2020 — readline bawaan sudah cukup.
4. Konversi dua arah dengan array JSON
Arah itu penting; terbalikkan dan semua perintah akan salah. Ingat satu aturan: array → JSONL adalah map (setiap rekaman dipadatkan jadi satu baris), JSONL → array adalah collect (baris dikumpulkan menjadi satu daftar).
# Array JSON → JSONL: -c memadatkan setiap elemen jadi satu baris
jq -c '.[]' records.json > records.jsonl
# JSONL → array JSON: -s mengumpulkan seluruh input menjadi satu array
jq -s '.' events.jsonl > records.json
# Menyaring JSONL baris demi baris: -s tidak perlu, jq memproses input sebagai stream
jq -c 'select(.active == true)' events.jsonl > active.jsonl
Satu baris perintah Python setara: array → JSONL menulis json.dumps(r, ensure_ascii=False) + "\n" per rekaman; arah sebaliknya mengumpulkan hasil json.loads baris demi baris ke daftar. Masing-masing arah punya satu jebakan. Arah pergi (array → JSONL), setiap baris baru asli di dalam rekaman harus di-escape oleh json.dumps — kalau tidak satu rekaman akan terkoyak menjadi beberapa baris. Arah balik (JSONL → array), jangan menyambung baris-baris menjadi satu string raksasa lalu mengurainya — dengan jutaan baris itu satu salinan penuh yang tidak perlu di memori. Agregasi (jumlah, pengelompokan, rata-rata) butuh -s yang memuat semuanya; pada file beberapa GB, beralihlah ke awk atau akumulator streaming di Python.
5. Enam jenis baris rusak dan cara mendiagnosisnya
Hampir semua error JSONL berada di level baris. Validasi baris demi baris dengan pelaporan nomor baris adalah satu-satunya diagnosa andal. Diurutkan berdasarkan frekuensi:
| Gejala | Akar penyebab | Perbaikan |
|---|---|---|
Extra data di akhir baris |
Koma ditambahkan di antara baris | Hapus; setiap baris harus berdiri sendiri |
| Satu rekaman terbelah menyeberangi baris | Baris baru asli tak ter-escape di dalam rekaman (diformat manual) | Padatkan ulang dengan json.dumps / jq -c |
Baris pertama gagal dengan Unexpected token ï |
File mengandung BOM | Simpan ulang sebagai UTF-8 tanpa BOM |
| Hanya baris terakhir beralat, padahal tampak benar | Baris baru penutup hilang atau ada spasi liar | Tambahkan \n; lewati baris kosong sesuai konvensi |
| Error baris baru di dalam string | Baris baru literal di dalam string, bukan \n |
Perbaiki generator; jangan pernah menyambung string JSON secara manual |
| Bilangan bulat besar berubah nilai | Diurai lewat double (terpotong di 2^53) | Gunakan parser yang memelihara presisi, lihat presisi bigint |
Rutinitas diagnosa selalu tiga langkah: head -n 1 untuk memastikan pengodean dan struktur baris pertama; jalankan skrip baris demi baris yang melaporkan nomor setiap baris rusak alih-alih berhenti di yang pertama; lalu keluarkan baris rusak satu per satu — satu baris adalah satu rekaman penuh, memeriksanya secara terpisah tidak menelan biaya apa pun. Untuk impor massal, kebijakan yang disarankan adalah "karantina, bukan menghentikan": tulis baris rusak beserta nomornya ke errors.jsonl, terus muatkan rekaman yang baik, lalu tentukan keputusan impor ulang berdasarkan proporsi baris rusak.
6. JSONL di atas HTTP: konsumsi baris demi baris dan backpressure
Pola khas NDJSON di atas HTTP: server menulis terus menerus, klien mengonsumsi terus menerus. Di sisi klien, jangan panggil response.json() — ia menyangga seluruh body sebelum penguraian, menghapus makna streaming. Baca stream body secara langsung, belah pada \n, dan bawa fragmen ekor yang belum lengkap ke iterasi berikutnya:
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split("\n");
buffer = lines.pop(); // potongan terakhir bisa jadi baris yang belum lengkap
for (const line of lines) {
if (line.trim()) handle(JSON.parse(line));
}
}
Di sisi server dua hal wajib: pertama, saat res.write() mengembalikan false, tunggu event drain (backpressure), kalau tidak memori tumbuh tanpa batas setiap kali konsumen lebih lambat daripada produsen; kedua, matikan middleware gzip pada rute streaming — gzip harus menyangga seluruh respons sebelum bisa memampatkan apa pun, dan itu melumpuhkan streaming NDJSON sepenuhnya. Kedua perilaku dikonfirmasi lewat uji lokal dan beberapa sumber independen berbahasa Inggris, bukan dugaan.
7. Kapan JSONL adalah pilihan yang keliru
Ruang negatif pemilihan format juga penting. Tiga skenario sebaiknya tetap memakai array JSON:
| Kebutuhan | Mengapa JSONL kurang cocok | Gunakan sebagai gantinya |
|---|---|---|
| Memvalidasi struktur seluruh dokumen | Tidak ada struktur keseluruhan; penyimpangan field luput terlihat | Array JSON + Schema |
| Akses acak ke rekaman ke-N | Panjang baris berubah-ubah; pencarian biner tak bisa | SQLite / Parquet |
| Penyarangan dalam, referensi antar rekaman | Setiap baris terisolasi; referensi tak punya tempat tinggal | Dokumen JSON standar |
Aturan saku satu kalimat: jika rekaman datang seiring waktu atau dikonsumsi baris demi baris (log, kejadian, ekspor, set pelatihan), gunakan JSONL; jika volume kecil dibuat sekali, dikonsumsi sekali, dan butuh semantik dokumen utuh, gunakan array JSON. Varian turunan (JSON Text Sequences dengan pemisah RS) adalah topik lain — lihat artikel RFC 7464.
FAQ
Apakah ada perbedaan nyata antara JSONL dan NDJSON?
Secara rekayasa, tidak. Keduanya berarti "satu nilai JSON lengkap per baris, tanpa pembungkus luar, tanpa koma di antara baris", dan alat yang mendukung satu pasti mendukung yang lain. Perbedaannya pada penamaan dan kebiasaan ekosistem: .jsonl mendominasi rekayasa data dan dataset ML; .ndjson dan application/x-ndjson mendominasi API HTTP streaming. Saat berintegrasi dengan sistem eksternal, ikuti ekstensi dan media type yang ditentukan dalam dokumentasinya.
Bisakah file .jsonl dibaca langsung dengan JSON.parse?
Bukan seluruh file. JSON.parse menerima tepat satu teks JSON, sedangkan JSONL adalah banyak nilai mandiri yang disusun baris demi baris, jadi penguraian seluruh file selalu gagal dengan "Unexpected token" atau "Extra data". Pecah menjadi baris dan urai setiap baris tak kosong secara terpisah, laporkan nomor baris bila gagal.
Jika satu baris file JSONL rusak, apakah seluruh file hancur?
Tidak — isolasi kerusakan per baris adalah inti desain format ini. Baris rusak hanya merugikan dirinya sendiri: lewati, catat nomornya, dan lanjutkan membaca. Untuk impor massal, karantina baris rusak ke file terpisah, muat rekaman yang baik seperti biasa, lalu tentukan perbaikan dan impor ulang berdasarkan proporsi baris rusak.
Bagaimana mengonversi JSONL ke Excel atau CSV?
Pertama ubah menjadi array atau ekstrak field baris demi baris, lalu jalankan konversi tabel: padatkan dengan jq -c '.[]' data.jsonl dan serahkan ke konverter, atau alirkan setiap baris lewat json.loads ke pandas. Hati-hati: field bersarang yang diratakan menghasilkan nama kolom bertitik, field array hanya bisa diserialisasi sebagai teks di dalam sel, dan kolom ID panjang sebaiknya dinyatakan sebagai string sejak awal untuk menghindari kehilangan presisi.