What Is NDJSON? β Definition and How It Works
Learn what NDJSON is, why one JSON object per line suits streaming and append-only collection, and the rules that keep it valid.
NDJSON β newline-delimited JSON β stores one complete JSON value per line. It is the standard format for streaming records, append-only logs, and datasets too large to hold in memory.
1. The Format
{"url":"https://example.com/1","title":"First","price":19.99}
{"url":"https://example.com/2","title":"Second","price":24.50}
{"url":"https://example.com/3","title":"Third","price":null}
Three rules cover it: one JSON value per line, no newlines inside a value, and no enclosing array or commas between lines.
2. Why Not a JSON Array
| Property | JSON array | NDJSON |
|---|---|---|
| Append a record | Rewrite the file | Append a line |
| Read without full parse | No | Yes, line by line |
| Memory to process | Whole document | One record |
| Partial file usable | No β invalid JSON | Yes, minus the last line |
| Concatenate two files | Requires re-wrapping | cat a b > c |
| Line-based tools | Unusable | Natural |
For collection specifically, the append and partial-file properties are decisive. A crawler writing an array must hold everything or rewrite the tail; a crawler writing NDJSON appends each record and a crash costs at most the final line.
3. Writing It
import json
with open("out.ndjson", "a", encoding="utf-8") as f:
for record in records:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
f.flush()
Two details matter more than they look. json.dumps must not be given indent, since pretty-printing inserts the newlines the format forbids. And flushing per record is what makes the file recoverable after a crash β buffered writes lose whatever had not reached disk.
4. Reading It
import json
with open("out.ndjson", encoding="utf-8") as f:
for line_no, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
print(f"malformed line {line_no}")
continue
process(record)
Skipping blank lines and tolerating one malformed line are what make a reader usable against real files, where the last line of an interrupted write is frequently truncated.
5. Command-Line Work
The format's other advantage is that ordinary tools apply:
wc -l out.ndjson # record count
jq -c 'select(.price != null)' out.ndjson # filter
jq -r '[.url, .title] | @csv' out.ndjson # to CSV
split -l 100000 out.ndjson part- # chunk
sort -u out.ndjson > deduped.ndjson # exact dedupe
Counting records with wc -l is a genuinely useful property: no parser, no memory, constant time in practice.
6. Compression and Storage
NDJSON compresses very well, because records share keys. Gzip is universal but not splittable; block formats such as bzip2 or zstd with frames allow parallel processing of one file.
For long-term analytical storage, converting to a columnar format costs far less space and reads far faster. The usual arrangement keeps compressed NDJSON as the raw append-only record and a columnar copy for querying.
7. Naming and Content Type
Both .ndjson and .jsonl are in wide use for the same format; application/x-ndjson is the conventional media type. Consistency within a project matters more than the choice.
8. Summary
NDJSON is one JSON value per line, with no wrapping array and no embedded newlines.
That makes it appendable, streamable, resilient to interruption, trivially concatenated, and usable with line-based tools β so write compact JSON with a flush per record, tolerate blank and truncated lines when reading, and keep a columnar copy when the data will be queried repeatedly.