JSON Lines và NDJSON: dữ liệu dạng luồng
Kết luận: Mỗi dòng một giá trị JSON mang lại khả năng ghi nối thêm, truyền luồng và cô lập hư hỏng trong một dòng; cái giá là thiếu cấu trúc tổng thể, nên việc kiểm tra phải diễn ra theo từng dòng.

Kết luận: dùng mảng JSON khi bạn xử lý cả lô cùng lúc, và dùng JSON Lines khi bạn cần ghi nối thêm dạng luồng cùng khả năng cô lập dòng lỗi; nếu vẫn phải kiểm tra cấu trúc tổng thể, đừng chuyển sang định dạng theo dòng.
Khi dữ liệu vượt quá bộ nhớ, hoặc cứ đến liên tục, cấu trúc "một mảng lớn" trở thành kẻ thù.
1. Quy tắc định dạng
JSON Lines (.jsonl) và NDJSON (.ndjson) thực tế là đồng nghĩa, và quy tắc của cả hai đều ngắn:
- Mã hóa là UTF-8, không có BOM;
- Mỗi dòng chứa một giá trị JSON hoàn chỉnh và hợp lệ;
- Các dòng được phân tách bằng
\n; dòng cuối có thể có hoặc không có ký tự xuống dòng; - Không được có ký tự xuống dòng chưa escape bên trong một dòng — xuống dòng trong chuỗi phải viết là
\n; - Các dòng trống thường bị bỏ qua, nhưng cách hiện thực nghiêm ngặt nên coi chúng là không hợp lệ.
{"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"}
Khác biệt tinh tế giữa hai bên: trang web JSON Lines nhấn mạnh "mỗi dòng một giá trị, và giá trị có thể là JSON bất kỳ (kể cả giá trị vô hướng)", trong khi đặc tả NDJSON bổ sung quy tắc hiện thực chặt hơn như "không có dòng trống và không có giá trị vô hướng ở cấp cao nhất". Trong kỹ thuật, bám theo bộ quy tắc chặt hơn sẽ giúp bạn tránh rắc rối.
2. So sánh với mảng JSON
| Khía cạnh | Mảng JSON | JSON Lines |
|---|---|---|
| Thêm một bản ghi | Phải ghi lại toàn bộ tệp (đổi dấu ] đóng) |
Chỉ cần nối thêm một dòng |
| Đọc dạng luồng | Cần trình phân tích dạng luồng theo dõi độ sâu ngoặc | Tách theo dòng |
| Một mục bị hỏng | Toàn bộ tệp không phân tích được | Chỉ dòng đó bị ảnh hưởng |
| Kiểm tra toàn tệp | Kiểm tra tất cả trong một lượt | Chỉ kiểm tra từng dòng |
| Nén | Kích thước tương đương sau gzip | Tương tự, và các khối có thể nén song song |
| Biểu diễn cấu trúc lồng nhau | Được hỗ trợ sẵn | Mỗi dòng có thể lồng bên trong; các dòng không liên quan nhau |
| Bên tiêu thụ điển hình | Mọi công cụ JSON | Hệ thống nhật ký, pipeline dữ liệu lớn, tập huấn luyện LLM |
Kết luận rất trực tiếp: dữ liệu là "một lô bản ghi" hay "một tài liệu" sẽ quyết định định dạng cần dùng.
3. Đọc: truyền luồng theo từng dòng
Trong Node, cách đơn giản nhất là 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 };
}
Mức sử dụng bộ nhớ của đoạn mã này chỉ phụ thuộc kích thước một dòng, không phụ thuộc tổng kích thước tệp — đó là giá trị lớn nhất của định dạng theo dòng.
3.1 Khi chỉ cần vài trường, đừng JSON.parse trước
Nếu mỗi bản ghi có 50 trường mà bạn chỉ cần 3, JSON.parse sẽ dựng đối tượng cho cả 50 trường.
Một trình phân tích dạng luồng (stream-json, clarinet) có thể chỉ trích ra các trường cần, với cái giá là độ phức tạp hiện thực cao hơn.
Quy tắc ngón tay cái: khi một bản ghi dưới vài trăm KB, gọi JSON.parse trực tiếp thường vẫn là lựa chọn tốt hơn.
3.2 Văn bản tiếng Trung và BOM
Các tệp .jsonl tạo trên Windows thường mang BOM, và JSON.parse trên dòng đầu chắc chắn sẽ lỗi. Hãy loại BOM trước khi đọc, hoặc kiểm tra ba byte đầu khi đang truyền luồng khối đầu tiên.
4. Ghi: nối thêm và tính nguyên tử
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' });
}
Hai điều cần lưu ý:
- Đầu ra của
JSON.stringifykhông chứa ký tự xuống dòng (trừ khi\ntrong một chuỗi bị escape thành hai ký tự), nên một dòng luôn đúng là một bản ghi; - Khi nhiều tiến trình ghi nối thêm đồng thời, một lệnh
writenhỏ hơn PIPE_BUF (thường là 4096 byte trên Linux) là nguyên tử; kích thước lớn hơn không được bảo đảm. Với tình huống đồng thời cao, hãy chuyển sang "mỗi tiến trình một tệp, gộp lại sau khi ghi xuống đĩa", hoặc dùng lưu trữ có giao dịch.
5. Xử lý dòng lỗi
Đây là quyết định mà định dạng theo dòng buộc bạn phải đưa ra trước:
| Chiến lược | Phù hợp với | Cái giá |
|---|---|---|
| Bỏ qua và đếm | Dữ liệu chịu được khoảng trống, như nhật ký và sự kiện phân tích | Dữ liệu mất âm thầm, nên cảnh báo là bắt buộc |
| Thất bại ngay | Dữ liệu không được phép mất, như thanh toán và hóa đơn | Một dòng lỗi chặn cả lô |
| Chuyển sang tệp thư chết | Trường hợp cần bổ sung sau | Thêm lưu trữ và quy trình phát lại |
Dù chọn cách nào, hãy ghi lại số dòng và một đoạn nội dung gốc, nếu không bạn không thể định vị vấn đề về sau. Số dòng không có trong thông báo lỗi của JSON.parse, nên bạn phải tự duy trì nó.
6. Nén và chia mảnh
- gzip nén
.jsonlvới tỉ lệ cao ổn định (tên trường lặp lại), nên khuyến nghị dùng.jsonl.gz; - gzip nén theo khối và hỗ trợ sẵn việc giải nén dạng luồng, nên không cần giải nén toàn bộ tệp;
- Chia tệp lớn thành các lát khoảng
1GB, có số thứ tự trong tên, để xử lý song song và truyền tiếp tục dễ hơn; - Đừng bao giờ tiền xử lý
.jsonlbằng cách "gộp thành một mảng" — làm vậy sẽ vứt bỏ mọi lợi thế của nó.
7. Cân nhắc so với các định dạng dữ liệu lớn khác
| Định dạng | Cấu trúc | Kèm lược đồ | Phù hợp với |
|---|---|---|---|
| JSON Lines | Độc lập theo từng dòng | Không (cần hợp đồng bên ngoài) | Nhật ký, nhập xuất, dữ liệu huấn luyện |
| CSV | Dạng bảng | Không | Báo cáo, trao đổi với Excel |
| Parquet | Dạng cột | Có | Truy vấn phân tích, tập dữ liệu ngoại tuyến rất lớn |
| Avro | Dạng hàng | Có | Tin nhắn Kafka, tiến hóa lược đồ |
Ưu điểm của JSON Lines là bất kỳ ngôn ngữ và công cụ nào cũng đọc ghi được ngay; nhược điểm là thiếu lược đồ và thiếu nén dạng cột. Khi dữ liệu không còn vừa một máy và bạn cần truy vấn theo cột thường xuyên, đã đến lúc cân nhắc Parquet.
8. Danh sách kiểm tra
- [ ] Phần mở rộng tệp nhất quán (
.jsonlhoặc.ndjson) và không bao giờ trộn lẫn; - [ ] Bên ghi bảo đảm "mỗi dòng một bản ghi", với ký tự xuống dòng trong chuỗi đã được escape;
- [ ] Bên đọc loại BOM, bỏ qua dòng trống và duy trì số dòng;
- [ ] Chiến lược xử lý dòng lỗi được định nghĩa rõ ràng và được giám sát (dòng lỗi / tổng số dòng);
- [ ] Tệp lớn được chia lát theo kích thước và đi kèm gzip cùng đọc song song;
- [ ] Nơi có hợp đồng trường, cung cấp thêm lược đồ và kiểm tra theo từng dòng.
FAQ
Tôi chọn mảng JSON hay JSON Lines?
Hãy xem dữ liệu là một lô bản ghi hay một tài liệu duy nhất. Chọn JSON Lines khi các bản ghi được ghi nối thêm và đọc dạng luồng: ghi nối thêm nghĩa là viết một dòng ở cuối, còn đọc chỉ cần tách theo dòng. Giữ mảng JSON khi toàn bộ cấu trúc phải được kiểm tra trong một lượt, vì mảng JSON kiểm tra tất cả cùng lúc trong khi JSON Lines chỉ kiểm tra được từng dòng.
Tại sao một dòng hỏng không làm hỏng toàn bộ tệp?
Mỗi dòng trong JSON Lines là một giá trị JSON hoàn chỉnh và độc lập, và việc phân tích vốn diễn ra theo từng dòng, nên dữ liệu hỏng chỉ ảnh hưởng dòng đó. Trong một mảng JSON, một lỗi cú pháp làm cả tệp không phân tích được, đó chính là lý do dữ liệu theo dòng phù hợp với nhật ký và sự kiện phân tích.
Tôi có nên chuyển JSON Lines thành mảng JSON không?
Có thể: đọc từng dòng, gọi JSON.parse cho mỗi bản ghi, và gom chúng vào một mảng. Làm vậy đòi hỏi toàn bộ nội dung phải vừa bộ nhớ và đánh mất việc đọc dạng luồng cùng khả năng nối thêm một dòng. Tiền xử lý .jsonl thành mảng sẽ vứt bỏ mọi lợi thế của định dạng, nên chỉ hợp lý cho việc giao hàng một lần khi dữ liệu vừa bộ nhớ.