NDJSON vs JSONL
NDJSON and JSONL are the same format. Same on-disk bytes, same parser, same MIME type, same use cases. The difference is the name and which ecosystem promotes it: NDJSON in JavaScript / observability / HTTP streaming, JSONL in Python / data engineering / ML. If you receive a file labeled one way and your tool expects the other, just rename the extension — there is no content-level conversion.
The short answer
- NDJSON = "Newline-Delimited JSON,"
.ndjson, promoted by ndjson.org. - JSONL = "JSON Lines,"
.jsonl, promoted by jsonlines.org. - LDJSON = "Line-Delimited JSON," older name, rare today.
All three: one JSON value per line, separated by \n, UTF-8, no enclosing array.
Side-by-side
| Aspect | NDJSON | JSONL |
|---|---|---|
| Full name | Newline-Delimited JSON | JSON Lines |
| Spec site | github.com/ndjson (community) | jsonlines.org |
| File extension | .ndjson | .jsonl |
| MIME type | application/x-ndjson | application/x-ndjson (most common); also application/jsonl |
| Encoding | UTF-8 | UTF-8 |
| Line separator | \n | \n |
| Per-line rule | One valid JSON value | One valid JSON value |
| Origin year | ~2013 | ~2013 |
| Dominant ecosystem | JavaScript / Node / npm, observability, HTTP streaming | Python, ML training (HuggingFace, OpenAI fine-tunes), data engineering |
| Where you'll see it | Elasticsearch _bulk, OpenAI / Anthropic streaming responses, pino / structlog / Vector / Fluentbit, D3 streams | OpenAI / Anthropic / Gemini / Llama / Mistral fine-tune datasets, HuggingFace datasets, BigQuery / Snowflake / DuckDB bulk imports |
| Common npm / pip name | ndjson, ndjson-cli | jsonlines (PyPI), jsonl (Cargo) |
| Spec coverage of blank lines | Silent (most parsers tolerate) | Silent (most parsers tolerate) |
| Spec coverage of trailing newline | Recommended, not required | Recommended, not required |
| Compressed extension | .ndjson.gz, .ndjson.zst | .jsonl.gz, .jsonl.zst |
The same file with both extensions
$ cat data.ndjson
{"id":1,"name":"Ada"}
{"id":2,"name":"Babbage"}
$ mv data.ndjson data.jsonl
$ cat data.jsonl
{"id":1,"name":"Ada"}
{"id":2,"name":"Babbage"}
Same bytes, two names. There is nothing to convert. The NDJSON ↔ JSONL normalizer exists only to clean up tangentially-related issues (BOM, CRLF, blank lines) and to let you download with either extension in one click.
Are there any differences in practice?
None at the format level. The minor practical differences come from each community's habits, not the format itself:
- Trailing newline conventions. The JSON Lines community more often emphasizes the optional trailing
\n; NDJSON tools sometimes omit it on streams (because the stream is "still open"). Both are valid. - Blank-line tolerance. Some strict NDJSON parsers (older Node
ndjsonversions) error on blank lines; most JSONL parsers silently skip them. Convention, not spec. - Streaming vs at-rest framing. NDJSON is more often discussed in the context of HTTP / chunked streaming. JSONL is more often discussed in the context of files at rest. Same bytes, different default mental model.
- Pretty-printing tolerance. Neither name allows multi-line records. Some "JSON streaming" libraries advertised under either name accept pretty-printed JSON by counting braces — that is non-conformant in both worlds.
If you stick to the conformance checklist (UTF-8 no BOM, \n endings, one minified JSON value per line, same shape every line, trailing newline), every NDJSON and JSONL consumer in the world will accept the file.
History
Both names emerged around 2013, formalizing conventions that had been in use for years:
- 2010s pre-history. Log shippers (Fluentd, Logstash), search engines (Elasticsearch
_bulk), and ETL pipelines were already writing "one JSON object per line" because it was the obvious thing to do. - 2013 — JSON Lines. jsonlines.org went up with a concise specification page. The
.jsonlextension started getting used in dataset releases. - 2013–2014 — NDJSON. The github.com/ndjson org began publishing npm packages under the NDJSON brand for the Node / streaming community.
- 2015–2018 — Co-existence. Both names spread through their respective ecosystems. The Python data community converged on JSONL; the JS / observability community on NDJSON.
- 2019–2023 — LLM era. OpenAI's fine-tuning API canonicalized
.jsonlas the dataset format, cementing JSONL in the ML world. Streaming-completion responses canonicalizedapplication/x-ndjsonas the wire format, cementing NDJSON in the API world. - 2024–present. The names are interchangeable in practice. The IANA-registered MIME type situation remains
application/x-ndjson(not formally registered, but universally recognized); no JSONL-specific MIME has displaced it.
Which name should I use?
Pragmatic guidance:
- Writing a streaming HTTP API or shipping logs? Call it NDJSON. Use
Content-Type: application/x-ndjsonand.ndjson. - Shipping a dataset for ML training, HuggingFace, BigQuery, or Snowflake? Call it JSONL. Use
.jsonl. - Building a tool that consumes either? Accept both extensions, document both names in your docs, and don't bikeshed the question.
- Writing for a broad audience? Lead with the one your audience knows and mention "also called X" once.
Will my tool work on both?
Almost certainly yes. Every parser worth using treats .ndjson and .jsonl identically. The few exceptions are usually tools that whitelist extensions (e.g., a CLI that only accepts .jsonl by name). In that case: rename the file. The bytes are identical.
This site's tools all accept both. The file pickers list both extensions; the validators flag the same issues; the converters output content that's valid under either name.
Conformance checklist (applies to both)
- UTF-8 without BOM.
\nline endings (not\r\n, not\r).- One JSON value per line; no embedded raw newlines inside records.
- No blank lines, no comments, no trailing commas, no
NaN/Infinity. - Same shape every line (heterogeneous is allowed but actively harmful).
- Trailing
\nafter the last record. - For HTTP:
Content-Type: application/x-ndjson, flush after each record.
Run any file through the NDJSON validator (or the equivalent JSONL validator) to check it against these rules.
FAQ
Should I migrate from NDJSON to JSONL (or vice versa)?
No migration is needed. The bytes are identical. If a tool requires a specific extension, rename the file. If you want to be community-aligned, match the dominant name in your ecosystem (NDJSON for JS / observability, JSONL for Python / ML / data).
Are there parsers that handle one but not the other?
Not at the format level. A few CLI tools whitelist extensions and will refuse a renamed file based on the extension string, not the bytes. That's a UX choice in the tool, not a format difference.
What's the difference in MIME type?
There isn't one in practice. application/x-ndjson is the universally-recognized MIME type for both names. application/jsonl appears in a handful of newer specs but is rarely used. Stick with application/x-ndjson.
Why two names?
Two independent communities formalized the same convention at around the same time, each producing a small website and a family of libraries. Neither could displace the other; both stuck. It's a Stigler's-law situation — neither name was "first" in any clean sense.
Where does LDJSON fit?
LDJSON ("Line-Delimited JSON") was an early name used briefly around 2012–2013. It got displaced by both NDJSON and JSONL and is rarely seen today. If you encounter an .ldjson file, treat it as either of the others.
— S., [email protected]