JSON Text Sequences (RFC 7464): JSON Streaming yang Dipisahkan RS
Intinya: RFC 7464 menggunakan pemisah 0x1E untuk men-streaming teks JSON yang dapat Anda baca dan tulis sekaligus. Mencakup aturan, perbedaannya dari NDJSON, dan implementasi pisah-lalu-parse.

Intinya: gunakan JSON-seq ketika Anda mengontrol kedua ujung, membutuhkan media type terdaftar, dan ingin pembaca menyinkronkan ulang setelah pemotongan; gunakan NDJSON ketika data memberi masukan ke sistem log, harus dapat dibaca manusia, atau harus bekerja dengan alat yang sudah ada — jangan pernah memaksakan JSON-seq pada rekan yang hanya memahami input per baris.
NDJSON memisahkan record pada baris baru, tetapi baris baru adalah konvensi manusia. RFC 7464 menuliskan batas itu ke dalam aliran byte itu sendiri: 0x1E tak terlihat sebelum setiap record.
1. Masalah yang dipecahkan oleh satu byte
RFC 7464 diterbitkan pada Februari 2015 dengan judul JSON Text Sequences, dan ia mendaftarkan media type application/json-seq. Ia tidak menambahkan tipe data baru. Ia hanya mendefinisikan bagaimana suatu urutan teks JSON disusun di dalam satu aliran byte. Seluruh aturannya muat dalam satu kalimat: letakkan satu RS sebelum setiap teks JSON dan satu LF setelahnya.
- RS adalah record separator ASCII, code point
0x1E, dikenal dalam Unicode sebagai INFORMATION SEPARATOR TWO (U+001E); - LF adalah line feed,
0x0A.
Dua record terlihat seperti ini pada tingkat byte:
1E 7B 22 69 64 22 3A 31 7D 0A ← 0x1E {"id":1} + LF
1E 7B 22 69 64 22 3A 32 7D 0A ← 0x1E {"id":2} + LF
RS berada sebelum record, yang merupakan kebalikan dari tata letak NDJSON "tulis konten, lalu tulis pemisah". Arah itu mengubah perilaku pemulihan: pembaca yang melihat 0x1E tahu record sebelumnya telah berakhir, sehingga bahkan record yang terpotong di tengah penulisan tidak dapat mengaburkan batas record berikutnya.
Ini bukan desain teoretis. RFC secara eksplisit menyebutkan record yang ditambahkan ke file log yang terpotong oleh filesystem setelah crash atau kill administratif, dan menyatakan bahwa parser sebaiknya tidak membatalkan seluruh pembacaan karena hal itu.
2. Dua kumpulan aturan: encoder dan parser
RFC 7464 secara tidak biasa menyediakan dua tata bahasa ABNF, satu yang membatasi penulis dan satu yang melonggarkan pembaca.
| Peran | Tata bahasa | Makna |
|---|---|---|
| Encoder | *(RS JSON-text LF) |
Satu RS sebelum setiap teks JSON, satu LF setelahnya |
| Parser | *(1*RS possible-JSON) |
Byte RS berurutan diperbolehkan, dan LF di antara record bersifat opsional |
Dalam istilah rekayasa, ada lima aturan keras:
- UTF-8 wajib; UTF-16 dan UTF-32 tidak boleh digunakan;
- Parser memperlakukan "byte apa pun selain RS" sebagai kemungkinan teks JSON dan mencoba mem-parse-nya; saat gagal ia melompat ke RS berikutnya dan melanjutkan;
- Byte RS berurutan tidak merepresentasikan elemen kosong dan diabaikan;
- Tidak ada penanda akhir urutan, sehingga aliran berakhir begitu saja;
- Ketika nilai tingkat atas adalah angka,
true,false, ataunull, record harus diakhiri dengan byte whitespace, jika tidak ia dibuang sebagai kemungkinan terpotong.
Aturan kelima adalah yang sering dilupakan orang. Objek, array, dan string bersifat self-delimiting dalam JSON; angka tidak. <RS>123<RS> tidak dapat memberi tahu Anda apakah ia memuat 123 lengkap atau 1234 yang terpotong. Karena itu RFC mengharuskan penulis menambahkan LF, menggunakannya sebagai byte kanari untuk deteksi pemotongan. Demikian pula <RS>true<RS> mungkin aslinya adalah trueish.
3. Memilih antara JSON-seq dan NDJSON
Kedua format melakukan streaming, tetapi menargetkan masalah berbeda:
| Dimensi | JSON Text Sequences | NDJSON / JSON Lines |
|---|---|---|
| Posisi pemisah | RS (0x1E) sebelum record |
LF (0x0A) setelah record |
| Media type | application/json-seq, terdaftar IANA |
Tidak ada pendaftaran resmi, konvensi komunitas |
| Keterbacaan byte | Karakter kontrol tak terlihat, cat tidak menampilkan apa pun |
Teks polos, langsung terbaca |
| Penambahan bersamaan | Pembaca dapat menyinkronkan ulang dan membuang ekor yang terpotong | Penambahan juga berfungsi, baris buruk dilewati |
| Angka tingkat atas | Membutuhkan byte whitespace di akhir agar dapat dipercaya | Line feed adalah batasnya |
| Pemisah di dalam string | RS hanya dapat muncul sebagai \u001e |
LF harus ditulis sebagai \n |
| Perkakas | jq --seq dan beberapa pustaka |
Hampir setiap alat log dan big data |
| Ekstensi file | Tidak didefinisikan oleh RFC | .jsonl / .ndjson |
Keputusannya singkat: gunakan JSON-seq ketika Anda mengontrol kedua ujung, menginginkan media type formal, dan membutuhkan pembaca untuk menyinkronkan ulang setelah pemotongan; gunakan JSON Lines dan NDJSON ketika data memberi masukan ke sistem log, perlu dibaca manusia, atau harus bekerja dengan alat yang sudah ada.
4. Parser: pisahkan pada 0x1E, lalu JSON.parse
Manfaat paling nyata RFC 7464 adalah Anda tidak memerlukan parser JSON streaming. Pisahkan berdasarkan byte, lalu panggil JSON.parse pada setiap segmen:
/** ASCII record separator, defined by RFC 7464 as 0x1E */
const RECORD_SEPARATOR = 0x1e;
/**
* Parses a chunk of JSON Text Sequence bytes.
* @param buffer Bytes that have been read so far
* @returns Successfully parsed records and the number of failed records
*/
function parseJsonSequence(buffer: Buffer): { records: unknown[]; badCount: number } {
const records: unknown[] = [];
let badCount = 0;
let recordStart = buffer.indexOf(RECORD_SEPARATOR);
while (recordStart !== -1) {
const nextRecordStart = buffer.indexOf(RECORD_SEPARATOR, recordStart + 1);
const recordEnd = nextRecordStart === -1 ? buffer.length : nextRecordStart;
const rawText = buffer.subarray(recordStart + 1, recordEnd).toString('utf8').trim();
if (rawText !== '') {
try {
records.push(JSON.parse(rawText));
} catch {
badCount += 1;
}
}
recordStart = nextRecordStart;
}
return { records, badCount };
}
Tiga catatan implementasi:
- Pemisahan tingkat byte itu aman. Byte lanjutan UTF-8 berada di rentang
0x80–0xBF, sehingga tidak pernah dapat sama dengan0x1E. Memisahkan pada 0x1E tidak dapat memotong karakter multi-byte menjadi dua. - Buffer ekornya saat streaming. Saat membaca dalam potongan, segmen terakhir mungkin belum mencapai RS berikutnya. Simpan di buffer sampai chunk berikutnya tiba; jika tidak, Anda akan mem-parse setengah record yang sama dua kali.
- Hitung kegagalan alih-alih membatalkan. RFC meminta parser melompat ke RS berikutnya dan melanjutkan, sehingga
badCountadalah metrik peringatan, bukan exception. Record terakhir yang terpotong juga termasuk di ember itu.
Penggunaan memori bergantung pada record tunggal terbesar plus satu buffer, bukan pada ukuran total file. Itu adalah gagasan yang sama yang dibahas di parsing dan streaming JSON besar.
5. Karakter kontrol di dalam string
JSON mengharuskan setiap karakter kontrol dari U+0000 hingga U+001F di-escape di dalam string, dan RS termasuk dalam rentang itu. Oleh karena itu string JSON yang valid hanya dapat memuat lima karakter \u001e, tidak pernah byte 0x1E mentah. Itulah sebabnya memisahkan pada 0x1E selalu aman untuk data yang valid.
Bahayanya berada di sisi penulis:
- Membangun teks JSON secara manual dengan
'\x1e', menyisipkan karakter kontrol mentah ke dalam string; - Membangun JSON dengan template literal alih-alih
JSON.stringify, meninggalkan baris baru, tanda kutip, dan karakter kontrol tanpa escape; - Menyalin teks yang mengandung RS dari terminal atau editor, tempat karakter kontrol tertelan atau diganti.
Pola yang benar selalu menghasilkan teks terlebih dahulu, lalu melampirkan pemisah:
const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;
JSON.stringify mengeluarkan karakter kontrol sebagai \u00XX, sehingga RS mentah tidak pernah dapat muncul di dalam record. Begitu langkah ini benar, logika pemisahan hanya perlu berurusan dengan byte dan tidak pernah perlu memahami sintaks JSON.
Sebagian besar kegagalan dalam kategori ini muncul sebagai JSON.parse yang melempar Unexpected token, terutama kasus membingungkan di mana "karakter pertama adalah byte kontrol tak terlihat". Tabel pencarian pesan error JSON membahas kelas error itu baris demi baris.
6. Transport HTTP dan media type
application/json-seq adalah media type terdaftar tanpa parameter wajib atau opsional, dan character set-nya tetap UTF-8. Pada HTTP, tiga praktik penting:
- Kirim
Accept: application/json-seqdan balas denganContent-Type: application/json-seq; - Untuk streaming sungguhan, server harus menggunakan chunked transfer dan menonaktifkan buffering respons di perantara seperti reverse proxy, CDN, dan alat pengujian beban;
- Klien yang membaca potongan demi potongan mungkin menerima chunk yang berakhir di tengah record, sehingga parser harus mem-buffer sampai RS berikutnya sebelum mengeluarkan record.
Jika pihak lain hanya memahami input per baris, jangan memaksakan JSON-seq padanya. Ia adalah standar, tetapi penetrasi ekosistemnya jauh di bawah NDJSON, sehingga kompatibilitas harus diputuskan selama negosiasi protokol alih-alih setelah kegagalan parse.
7. Command line, log, dan dukungan alat
jq adalah salah satu dari sedikit alat dengan dukungan native; flag --seq-nya ada justru untuk RFC 7464:
printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'
Itu mencetak 1 dan 2 pada baris terpisah. Untuk endpoint streaming, ingat untuk menonaktifkan buffering curl:
curl -sN https://api.example.com/events | jq --seq -c '.type'
jq --seq membaca input RFC 7464, mengabaikan record buruk, dan terus mem-parse. Itu sangat berguna ketika mengikuti file log yang masih ditulis: record terakhir mungkin baru setengah tertulis, dan output tidak akan menampilkan objek ekor palsu.
7.1 Dukungan native per lingkungan
| Lingkungan | Dukungan bawaan | Catatan |
|---|---|---|
| jq | --seq |
Opsi command-line paling mudah |
| Node.js | Tidak ada | Buffer.indexOf plus JSON.parse |
| Python | Tidak ada | Pisahkan pada b'\x1e' |
| Go | Tidak ada | SplitFunc kustom untuk bufio.Scanner |
| Peramban | Tidak ada | fetch plus ReadableStream, pisahkan byte secara manual |
7.2 Jebakan umum
Kesalahan mengelompok di sisi pembacaan:
- Membaca dengan
readline, yang meninggalkan awalan RS tak terlihat di baris pertama sehinggaJSON.parseselalu gagal; - Membuka file
.json-seqdi editor tempat karakter kontrol tak terlihat, sehingga tampak seperti satu dokumen JSON yang aneh; - Memisahkan dengan
split(/\n/), yang mencampur baris baru yang di-escape di dalam record dengan LF di antara record; - Memperlakukan RS sebagai karakter biasa dalam dokumentasi, sehingga ia menghilang setelah copy dan paste.
8. Kapan layak digunakan
Cocok untuk:
- Anda memiliki produsen dan konsumen sekaligus, tanpa alat pihak ketiga di antaranya;
- Data adalah aliran event yang terus ditambahkan dan prosesnya mungkin crash, sehingga Anda perlu pemulihan dari pemotongan;
- Protokol layak mendapat media type formal alih-alih konvensi seperti
.jsonl; - Record individual mungkin angka atau boolean tingkat atas, di mana aturan pemotongan eksplisit penting.
Kurang cocok untuk:
- Log harus masuk ke ELK, Loki, atau sistem lain yang hanya memahami input per baris;
- Orang perlu
cat,grep, danawkdata secara langsung; - Pihak lain hanya dapat mem-parse
application/json.
9. Checklist
- [ ] Penulis menghasilkan record dengan
JSON.stringify, lalu menambahkan0x1Edi depan dan\ndi belakang; - [ ] Record yang memuat angka, boolean, atau
nulltingkat atas diakhiri dengan LF; - [ ] Pembaca memisahkan berdasarkan byte alih-alih membaca baris demi baris;
- [ ] Pembacaan streaming menyimpan ekor yang belum tertutup di buffer sampai RS berikutnya tiba;
- [ ] Kegagalan parse dihitung dan dilewati, tidak pernah fatal bagi urutan;
- [ ] Antarmuka publik menyatakan
application/json-seqsecara eksplisit dan menyimpan fallback NDJSON.
FAQ
Haruskah saya mengadopsi JSON-seq untuk endpoint API publik?
Hanya ketika kedua ujung dapat diubah atau format dinegosiasikan di awal. JSON-seq adalah standar terdaftar dengan media type application/json-seq, tetapi penetrasi ekosistemnya jauh di bawah NDJSON, sehingga memaksakannya pada klien pihak ketiga biasanya gagal. Putuskan kompatibilitas selama negosiasi protokol dan pertahankan fallback NDJSON.
Haruskah saya mengonversi pipeline NDJSON yang ada ke JSON-seq?
Biasanya tidak. NDJSON sudah melakukan streaming, penambahan, dan melewati baris buruk, dan hampir setiap alat log serta big data menerimanya. Konversi hanya ketika Anda membutuhkan media type terdaftar IANA atau sinkronisasi ulang yang disediakan pemisah di depan, dan hanya ketika produsen dan konsumen dapat diubah bersama-sama.
Mana yang lebih umum, JSON Text Sequences atau NDJSON?
NDJSON. Aturannya lebih sederhana, cakupan alatnya jauh lebih luas, dan sistem log serta pipeline big data umumnya menerimanya. JSON-seq unggul pada media type terdaftar IANA dan pada sinkronisasi ulang yang berasal dari menempatkan pemisah di depan, yang cocok untuk protokol streaming terkendali alih-alih pertukaran data umum.
Mengapa pemisah ditempatkan sebelum record alih-alih setelahnya?
Karena record dapat terpotong. Dengan pemisah di depan, pembaca yang melihat RS tahu record baru telah dimulai, sehingga record sebelumnya tidak dapat merusak yang berikutnya baik ia lengkap maupun tidak. Dengan pemisah di belakang, penulisan yang terinterupsi meninggalkan segmen akhir yang tidak memiliki penanda akhir maupun penanda awal berikutnya, sehingga batasnya hanya dapat ditebak.
Apa yang terjadi jika 0x1E muncul di dalam record?
String JSON yang valid harus meng-escape RS sebagai \u001e, sehingga 0x1E mentah seharusnya tidak pernah muncul. Jika muncul, teksnya tidak lagi JSON yang valid: pemisahan pada 0x1E akan rusak di tempat yang salah dan JSON.parse akan gagal. Perbaikannya berada di sisi penulis: gunakan JSON.stringify alih-alih teks yang dibangun manual.
Bisakah peramban mem-parse application/json-seq secara langsung?
Tidak. Peramban tidak membawa parser JSON-seq; fetch hanya mengirimkan byte. Anda perlu menggunakan response.body.getReader(), mengakumulasi chunk sampai 0x1E berikutnya, dan memanggil JSON.parse pada setiap segmen. Untuk payload kecil, membaca seluruh body sebagai string terlebih dahulu lalu memisahkannya setelahnya juga berfungsi.
Mengapa angka tingkat atas membutuhkan LF di akhir?
Objek, array, dan string bersifat self-delimiting dalam JSON, sementara angka tidak: <RS>123<RS> bisa jadi 123 lengkap atau 1234 yang terpotong. Karena itu RFC 7464 mengharuskan byte whitespace setelah setiap record, dan LF bertindak sebagai kanari untuk deteksi pemotongan. Aturan yang sama berlaku untuk true, false, dan null tingkat atas.