创建日期:2026-09-22 | 最近更新:2026-09-22 规范条款为逐字原文(NDJSON Spec / jsonlines.org / RFC 7464,抓取日期 2026-09-22,留档于
source/jsonl-lab/official-sources.md);数字为本机实测(Node 24.19.0,脚本source/jsonl-lab/harness.mjs+harness.log)。两类陈述在正文中分别标注。
JSONL 是什么:一行一个 JSON
一句话:JSONL(JSON Lines,也叫 NDJSON)就是"一行放一个完整的 JSON,行与行之间用换行分隔"的文本格式。 它不是一个新语法——每行单独拿出来都是合法的标准 JSON;它的全部价值在于把"一份文档"变成了"一串可以逐条处理的记录"。
理解它的最快方式是先认清一件事:JSONL 主要不是为了省空间,也不是为了更好看,而是为了一种访问模式——边产出边消费、追加写、坏一行不连坐、能直接用 grep 和 head。 同一份数据写成 JSONL 和写成 JSON 数组,体积几乎一样大(实测见 §3.1),差别全在你怎么用它。
1. 它长什么样
同一个数据结构,两种写法:
// ① JSON 数组:一整份文档
[
{"id": 1, "role": "user", "text": "第一条"},
{"id": 2, "role": "assistant", "text": "第二条"}
]
// ② JSONL:一行一条,没有外层方括号,没有逗号
{"id":1,"role":"user","text":"第一条"}
{"id":2,"role":"assistant","text":"第二条"}
关键差别不在"看起来",而在解析器必须怎么读它:
| JSON 数组 | JSONL | |
|---|---|---|
| 何时算"可解析" | 必须拿到整份文档(要看到 ]) | 拿到一行就能解析一条 |
| 追加一条 | 读全量 → 改结构 → 重写全量 | >> 追加一行 |
| 有一条坏了 | 整份文档解析失败 | 只坏那一行 |
能否 grep / head -1 | 不能(不是行结构) | 能 |
2. 规范怎么说(逐字,且它没有 RFC)
先说一个容易被忽略的事实:JSONL 不是 IETF 标准,没有 RFC。 现在并存着两套"社区规范",而且它们互相打架。
2.1 NDJSON Spec v1.0.0 —— 逐字
【官方原文】"Each JSON text MUST conform to the [RFC8259] standard and MUST be written to the stream followed by the newline character
\n(0x0A). The newline character MAY be preceded by a carriage return\r(0x0D). The JSON texts MUST NOT contain newlines or carriage returns." "All serialized data MUST use the UTF8 encoding." —— NDJSON Spec v1.0.0 §3.1(last_update: 2014-10-19)
【官方原文】"The parser MUST accept newline as line delimiter
\n(0x0A) as well as carriage return and newline\r\n(0x0D0A)." "If the JSON text is not parsable, the parser SHOULD raise an error. The parser MAY silently ignore empty lines, e.g.\n\n. This behavior MUST be documented and SHOULD be configurable by the user of the parser." —— NDJSON Spec §3.2
【官方原文】"The MediaType for Newline Delimited JSON SHOULD be application/x-ndjson." / "When saved to a file, the file extension SHOULD be .ndjson." —— NDJSON Spec §3.3
NDJSON 自述的立项动机也值得抄下来——它承认这是个空缺:
【官方原文】"There is currently no standard for transporting instances of JSON text within a stream protocol, apart from [Websockets], which is unnecessarily complex for non-browser applications." "A common use case for NDJSON is delivering multiple instances of JSON text through streaming protocols like TCP or UNIX Pipes. It can also be used to store semi-structured data." —— NDJSON Spec §1.1
2.2 JSON Lines(jsonlines.org)—— 逐字
【官方原文】"JSON Lines is a convenient format for storing structured data that may be processed one record at a time. It works well with unix-style text processing tools and shell pipelines. It's a great format for log files. It's also a flexible format for passing messages between cooperating processes."
三条要求(逐字):
【官方原文】
- UTF-8 Encoding … "Like the JSON standard a byte order mark (U+FEFF) must NOT be included."
- Each Line is a Valid JSON Value … "any JSON value is permitted. e.g.
nullis a valid value but a blank line is not."- Line Terminator is '\n' … "'\r\n' is also supported because surrounding white space is implicitly ignored when parsing JSON values." / "Including a line terminator after the last JSON value in a file is strongly recommended but not required." —— jsonlines.org
2.3 ★ 两套规范打架的地方(这点很重要)
| 细节 | NDJSON Spec | jsonlines.org | 现实怎么办 |
|---|---|---|---|
| 扩展名 | SHOULD 用 .ndjson | 用 .jsonl | .jsonl 事实上更常见(见 §5 本机实测) |
| MIME | SHOULD 用 application/x-ndjson | 可以是 application/jsonl,但"not yet standardized" | 客户端别硬校验 Content-Type |
| 空行 | parser MAY silently ignore(须可配置) | "a blank line is not" 合法值 | 读的时候容忍、写的时候别产生 |
| 末行换行 | 每条 MUST 跟 \n | "strongly recommended but not required" | 一律补上(理由见 §6 坑 3) |
| BOM | 未提 | must NOT 包含 | 写入时确保无 BOM |
所以凡看到"规范要求…",都要先问一句:哪一份规范? 本文引用时都注明了出处。
2.4 唯一的 IETF 标准走的是另一条路
IETF 有一份 Standards Track 的"多条 JSON"格式,但它不是换行分隔:
【官方原文】"A JSON text sequence consists of any number of JSON texts, all encoded in UTF-8, each prefixed by an ASCII Record Separator (0x1E), and each ending with an ASCII Line Feed character (0x0A)." —— RFC 7464 · JSON Text Sequences(2015-02)
它给每个 JSON 前面加一个 RS 控制字符(0x1E),MediaType 是 application/json-seq。别把 JSON-seq 当 JSONL 解析——前导 RS 会让 JSON.parse 直接失败。
3. 为什么用它:六条理由,各配一个"数组做不到"
3.1 追加写:JSONL 只写一行,数组要重写整份(实测)
50000 条记录,追加 1 条:
JSONL :写入 82 字节(就这一行),耗时 0.4 ms
JSON 数组:重写 6065841 字节(整份),耗时 155.0 ms
→ 追加成本差 435 倍量级
【本人实测】这是本机
/tmp(tmpfs)上的数字,只说明机制:append-only 是 JSONL 的原生用法,数组的追加天然是 O(文件大小)。
顺便破除一个常见误解:JSONL 并不更小。 同一份 50000 条数据:
JSON 数组 5923.6 KB
JSONL 5923.6 KB ← 结构性开销只差 50000 字节(逗号 vs 换行)
【本人实测】体积上二者几乎等价;JSONL 的优势是访问模式,不是压缩率。
3.2 流式:拿到第一行就能用,不必等整份文档(实测)
同样是 50000 条:
想拿到"第 1 条",各需要先读入多少字节:
JSON 数组 → 6065759 字节(JSON.parse 要整份文档)
JSONL → 111 字节(第一行)
按 4KB 分块喂入时,JSONL 在第 1 块就解析出了第 1 条。
**【本人实测】**这一条跟本站刚写的流式响应的分帧与截断是同一个道理:逐行/逐帧的格式才能边到边用。区别只是那里切帧靠空行(SSE),这里切记录靠换行。
3.3 逐行容错:坏一行不连坐
JSON 数组里多一个逗号,整份文档废掉;JSONL 里第 3 行坏了,其余 49999 行照常可用。日志场景这是决定性的——你要的是"尽量读出能读的",不是"要么全有要么全无"。
3.4 可以被 Unix 工具直接处理
head -1 events.jsonl # 看第一条长什么样
grep '"role":"tool"' events.jsonl # 只捞工具调用
wc -l events.jsonl # 数条数
tail -f events.jsonl # 实时跟日志
JSON 数组做不到——它不是行结构,grep 出来的片段没法单独解析。这也是 jsonlines.org 说的 "works well with unix-style text processing tools and shell pipelines"。
3.5 并行/分片友好
按行切分即可分给多个 worker 处理,条与条之间无状态耦合(数组则要先整体 parse 再分片)。
3.6 边产出边写
生成端可以每产出一条就 append 一次,不需要在内存里攒成一个数组再序列化。最生动的例子就在本机:
目录:~/.claude/projects/<本项目>/
文件数 7,合计 7174.3 KB,合计 3466 行
平均每行 2120 字节 ← 一行可以很大(一条消息可能带整个工具结果)
抽样首行顶层键:type, mode, sessionId
【本人实测】这是 Claude Code 的会话记录文件,全部是
.jsonl。而且实验跑两次数字会变(第一次 3439 行、第二次 3466 行)——因为正在写这篇文章的这个会话本身,就在被一行行 append 进去。这就是 append-only JSONL 的现场演示。
4. 四种"多条 JSON"的路线对比
| 方案 | 分隔方式 | 流式/追加 | 标准 | 适用 |
|---|---|---|---|---|
| JSON 数组 | 逗号 + 方括号 | 都要整份 | RFC 8259 | 配置、单次传输、结构本身是树 |
| JSONL / NDJSON | 换行 \n | 原生支持 | 社区规范(两份,互相打架) | 日志、数据集、消息流、批量任务 |
| JSON-seq(RFC 7464) | 前置 RS 0x1E | 支持 | IETF Standards Track | 需要 IANA 注册的媒体类型、严格管道 |
| CSV / TSV | 换行 + 逗号 | 支持 | RFC 4180 | 二维表格、人眼/Excel;无类型、无嵌套 |
选择逻辑很简单:数据是"记录流"就用 JSONL;是"一棵树"就用 JSON;是"一张表"就用 CSV。 需要正式媒体类型注册、又必须逐条的话,看 RFC 7464。
5. 谁在用(都是可核对的事实)
- 本机实测:这台机器上有 35 个
.jsonl文件,抽样几类完全不同的用途——- Claude Code 的会话记录(
~/.claude/projects/**.jsonl); - 微调数据集(
MOSS-TTS-Nano/finetuning/datasets/*.jsonl); - LLM 请求日志(
VideoCaptioner/AppData/logs/llm_requests.jsonl); - 任务运行状态(pnpm 的 task-run-state
.jsonl)。
- Claude Code 的会话记录(
- 官方示例代码:OpenAI 官方 cookbook 的 Batch 处理示例里,输入/输出文件名就是
batch_tasks_movies.jsonl、data/batch_job_results_movies.jsonl(来源:openai/openai-cookbook仓库examples/batch_processing.ipynb,抓取日期 2026-09-22)。 - 规范自述:NDJSON 说它的用场是"TCP/UNIX pipe 这类流协议"与"半结构化数据存储";jsonlines.org 说它"很适合日志文件"。
【说明】各厂商文档(OpenAI / Anthropic 的 API reference)在本机抓不到正文(403 / 客户端渲染 + 区域限制), 所以这里不引用它们"Batch 输入必须是 JSONL"之类的措辞;上面两条分别标了来源。
6. 七个坑(全部实测复现)
坑 1:一"美化"就不是 JSONL 了
JSON.stringify(records, null, 2) // ✗ 缩进把每条记录摊成多行
JSON.stringify(records) // ✓ 一行一条
期望解析出 3 条;按行解析实际得到:成功 0 条、失败 23 行
【本人实测】"一行一条"是前提,缩进直接破坏前提。写文件用
JSON.stringify(obj),不要带第三参数。
坑 2:字段里含真实换行——手拼字符串必炸
手拼字符串 :物理行数 2,按行解析 成功 0 / 失败 2
JSON.stringify:物理行数 1,按行解析 成功 1 / 失败 0
【本人实测】规范原文说得很直接:JSON texts "MUST NOT contain newlines or carriage returns"。
JSON.stringify会把字符串里的换行转义成\n两个字符(不是真换行),所以永远用它产出,别手拼。
坑 3:★ 末尾那个换行符——不同读法结果不同(实测)
一份数据、三种读法:
数据 循环 indexOf split('\n') node:readline
有结尾换行 3 3 3
无结尾换行 2 3 3
【本人实测】"文件末尾没有换行符"时,循环
indexOf('\n')的写法会静默丢掉最后一条记录—— 不报错、不警告,就是少一条。这就是 NDJSON 把"每条后面跟\n"写成 MUST 的原因。 写的时候补上末尾换行,是这里唯一的稳妥做法。
坑 4:截断的尾行必须丢弃
进程被杀、磁盘写满、流被掐断,都可能留下半行 {"a":。规范(NDJSON §3.2)说不可解析就 SHOULD 报错,
而 jsonlines.org 的"每行都是合法 JSON 值"也意味着半行不是记录——必须丢掉,不要尝试"修复"。
【本人实测】
{"a":1}\n{"a":→ 成功 1 条,残渣{"a":被丢弃。 这与流式响应里"结尾半帧按规范丢弃"是同一个机制(见那篇)。
坑 5:BOM 会让第一行解析失败
UTF-8 BOM(U+FEFF)在文件开头,会让"第一行"变成 {"a":1}:
开头带 UTF-8 BOM 成功 0 / 失败 1
**【本人实测】**jsonlines.org 明确 "a byte order mark (U+FEFF) must NOT be included"。 读的时候如果来源不可控,先 strip 掉 BOM 再按行解析。
坑 6:空行——两份规范态度不一
NDJSON 说 parser MAY silently ignore 空行(但要文档化、可配置);jsonlines.org 说空行本身就不是合法值。实测三种写法里,按行跳过空行都能正常工作,但这属于"我的解析器选择",换库时要确认它的默认行为。
坑 7:扩展名与 Content-Type 没有权威答案
.jsonl 还是 .ndjson?application/jsonl 还是 application/x-ndjson?两份规范各说各的(§2.3 表)。
工程上的做法:写 .jsonl(最常见),读的时候别硬校验扩展名/Content-Type,按内容判断。
附:一个不在格式层、但一定会遇到的坑
大整数精度。 JSON.parse 走的是 IEEE 754 双精度,超出安全整数范围的 ID(如雪花 ID、int64)会静默丢精度——
JSONL 常用来装导出数据,这个坑更容易中。同类问题在高性能 SQLite 那篇里也记过(四种驱动的处理各不相同)。
7. Node 里怎么写、怎么读
写:一行一条,末尾带换行
import { appendFile } from 'node:fs/promises';
// 关键三点:JSON.stringify 不带缩进;自己补 '\n';用 append 而不是重写
async function appendRecord(path, record) {
await appendFile(path, JSON.stringify(record) + '\n', 'utf8');
}
读:首选 readline(自动处理"末尾无换行")
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
for await (const line of createInterface({ input: createReadStream(path) })) {
if (line.trim() === '') continue; // 跳过空行(可配)
try { handle(JSON.parse(line)); }
catch { /* 坏行/截断尾行:记一笔,继续 */ }
}
node:readline会把"末尾没有换行"的最后一行照常交给你(实测见坑 3)——这正是手写indexOf('\n')循环最容易翻车的地方。
手写流式读取(要自己处理残行)
需要从网络流里边到边解析时,骨架和读 SSE 完全一样:
攒字节 → 切行 → 残段留到下一轮 → 解码用 TextDecoder(..., {stream:true})。
let buf = '';
for await (const chunk of stream) {
buf += dec.decode(chunk, { stream: true });
let i;
while ((i = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, i);
buf = buf.slice(i + 1); // 残段留在 buf
if (line.trim()) try { handle(JSON.parse(line)); } catch {}
}
}
// 流结束时 buf 里若还有内容:那是没换行结尾的最后一条,
// 要么按 readline 的做法也收下,要么明确丢弃——但必须是"有意识的选择"。
8. 什么时候不要用 JSONL
| 你的需求 | 更该用什么 |
|---|---|
| 需要"整份文档原子更新"(要么全改,要么不改) | 单个 JSON 文档 + 原子替换(写临时文件再 rename) |
| 需要随机访问、按条件查询、聚合 | SQLite(见高性能 SQLite)/ Parquet / 数据库 |
| 数据本质是一棵树、且不会被逐条处理 | 普通 JSON |
| 列式分析、体积敏感、要压缩率 | Parquet(列存 + 编码压缩) |
| 人眼/Excel 直接看 | CSV |
一句话判据:如果消费方需要"整体语义"(事务、查询、聚合),JSONL 就不合适——它的强项恰恰是"每条独立"。
9. 一句话总结
JSONL 不是"更好的 JSON",而是"把 JSON 当成记录流来用"的一种约定。 它几乎不省空间, 换来的是追加写、逐行流式、单行容错、以及能被
grep/head/tail直接处理的朴素结构。 代价是没有外层结构、没有 schema、没有权威规范——所以约定俗成的那几条(每行一个JSON.stringify、末尾补\n、不缩进、不要 BOM、空行容忍)就成了你必须自己守住的东西。
关联
- 逐行/逐帧解析的同构问题:大模型流式 chunk 会因"网络波动"乱序吗(分帧、残帧丢弃)
- 装 JSON 数据的另一端:高性能 SQLite(大整数精度、写入代价)
- 消息本身的格式:OpenAI 与 Anthropic 消息格式、流式 SSE 与打字机
- 实测脚本与逐字来源:仓库内
source/jsonl-lab/(harness.mjs/harness.log/README.md/official-sources.md)
自测
- JSONL 与 JSON 数组相比,体积有明显优势吗?那它的优势到底在哪?
- 为什么说"JSONL 没有 RFC"?现存的两份社区规范在扩展名和末行换行上分别怎么说?
- RFC 7464(JSON-seq)用什么做分隔?为什么不能当 JSONL 解析?
JSON.stringify(x, null, 2)写出来的东西为什么不是 JSONL?- 字段文本里包含换行时,为什么用
JSON.stringify就没事、手拼字符串就炸? - 文件末尾没有换行符时,循环
indexOf('\n')的读法会怎样?node:readline呢? - 截断的半行
{"a":该怎么处理?依据是什么? - 什么情况下不该用 JSONL?
参考
- NDJSON Spec v1.0.0:github.com/ndjson/ndjson-spec(
last_update: 2014-10-19) - JSON Lines:jsonlines.org(三条要求与约定)
- RFC 7464 · JSON Text Sequences(Standards Track,
application/json-seq,RS 0x1E 分隔):rfc-editor.org/rfc/rfc7464.html - RFC 8259 · JSON 本身:rfc-editor.org/rfc/rfc8259.html
- 官方示例代码中的
.jsonl:openai/openai-cookbook的examples/batch_processing.ipynb - 逐字来源留档与实测输出:
source/jsonl-lab/official-sources.md、source/jsonl-lab/harness.log