NDJSON (newline-delimited JSON), also called JSON Lines or JSONL, is a simple convention: a file or stream where every line is a complete, independent JSON value, usually an object. It's the standard format for application logs, data exports, bulk APIs and streaming pipelines, because it can be written, read and processed one record at a time.
text{"ts":"2026-01-01T12:00:00Z","level":"info","msg":"started"} {"ts":"2026-01-01T12:00:01Z","level":"warn","msg":"slow request","ms":1840} {"ts":"2026-01-01T12:00:02Z","level":"error","msg":"timeout","route":"/api/orders"}
Each line is valid JSON on its own. The file as a whole is not a single JSON document. There's no surrounding array and no commas between records.
\n (a trailing newline after the last record is recommended).\n, which JSON serializers do automatically..ndjson and .jsonl.| JSON array | NDJSON | |
|---|---|---|
| Shape | [{...}, {...}, {...}] | One {...} per line |
| Append a record | Rewrite the closing ] | Add a line |
| Read a partial file | Fails to parse | Every complete line is usable |
| Memory | Usually loads everything | One record at a time |
| Standard tools | Needs a JSON parser | grep, head, wc -l, split work |
If a process crashes halfway through writing a JSON array, the file is invalid. With NDJSON, you only lose the last incomplete line.
_bulk endpoint, and batch inference or embedding APIs that accept a .jsonl file.bash# Count records wc -l events.ndjson # Show only errors grep '"level":"error"' events.ndjson # Filter properly with jq (jq reads one value per line natively) jq -c 'select(.level == "error")' events.ndjson # Convert a JSON array to NDJSON jq -c '.[]' events.json > events.ndjson # Convert NDJSON back to a JSON array jq -s '.' events.ndjson > events.json
pythonimport json with open("events.ndjson", encoding="utf-8") as f: for line in f: if line.strip(): event = json.loads(line)
javascriptimport { createReadStream } from 'node:fs'; import { createInterface } from 'node:readline'; const lines = createInterface({ input: createReadStream('events.ndjson') }); for await (const line of lines) { if (line.trim()) handle(JSON.parse(line)); }
Skipping blank lines makes the reader tolerant of a trailing newline.
To look at one record closely, copy a line into the JSON Formatter and click Format. If a line won't parse, the most common causes are a record that was split across two lines, or a truncated last line from a process that was killed mid-write. See Validate JSON for other errors.
For large single documents that you need to break up, see Handle large JSON.