JSON to NDJSON: Convert a JSON Array to JSON Lines (and Back)
Turn a JSON array into NDJSON (JSON Lines) with one object per line for logs, streaming, Elasticsearch bulk loads, and ML datasets, then convert back.
JSON to NDJSON: One JSON Object Per Line, and How to Get There
A JSON array and an NDJSON file can hold the exact same data, yet half the tools in a data pipeline accept only one of them. Export a table from one system and it hands you [{...},{...},{...}]. Try to load that into BigQuery or push it through an Elasticsearch bulk endpoint and the loader rejects it, because those tools want NDJSON: one complete JSON value on each line, with no commas between rows and no outer brackets. This post explains the difference, walks through a real conversion, and shows when each format is the right call.
What NDJSON Actually Is
NDJSON stands for newline-delimited JSON. You will also see it written as JSONL or JSON Lines; they are the same thing. The rule is short: every line is one independent JSON value, parsed on its own, with nothing wrapping the whole file.
That single rule is the whole point. In a JSON array, [, ,, and ] are structural. A parser cannot understand row two until it has read row one and the comma between them, and it cannot declare the document valid until it reaches the closing bracket. NDJSON throws all of that away. Each line stands alone, so a reader can consume line 1, hand it off, forget it, and move to line 2 without holding the rest of the file in memory.
Here is the contrast on three records:
A JSON array:
[
{"id": 1, "event": "login", "ms": 42},
{"id": 2, "event": "search", "ms": 88},
{"id": 3, "event": "logout", "ms": 17}
]
The same data as NDJSON:
{"id":1,"event":"login","ms":42}
{"id":2,"event":"search","ms":88}
{"id":3,"event":"logout","ms":17}
Three lines, three standalone objects, no commas, no brackets. Append a fourth event and you just write one more line. You never reopen the file to fix a trailing comma or move a closing bracket, which is exactly why append-only systems love it.
Why Pipelines Prefer One Object Per Line
The advantage shows up the moment data arrives or is processed a record at a time.
- Logs. A log shipper appends a line per event. Crash mid-write and you lose the last partial line, not the entire file, because nothing has to close a bracket for the rest to stay valid.
- Streaming. An HTTP streaming endpoint can flush a line the instant a row is ready. The client starts working on record 1 while record 500 is still being computed.
- Bulk loaders. A 5 GB JSON array forces the loader to read the whole thing before the first object is usable. The same 5 GB as NDJSON is processed line by line with near-constant memory.
- ML datasets. Training pipelines iterate one example per step. NDJSON maps cleanly to that loop, and you can shuffle, shard, or
head -n 1000a file without parsing it.
Concrete consumers that expect this shape: BigQuery bulk loads want NEWLINE_DELIMITED_JSON, the Elasticsearch and OpenSearch _bulk API speaks NDJSON, jq emits it by default, and pandas reads it with read_json(..., lines=True). When a service tells you to upload "JSONL" or "one JSON object per line," NDJSON is what it means.
A Worked Conversion
Say you exported a small events table as an array and a loader rejected it. Paste this into the JSON to NDJSON converter and switch to array-to-NDJSON:
Input:
[{"user":"ana","plan":"pro","seats":5},{"user":"ben","plan":"free","seats":1}]
Output:
{"user":"ana","plan":"pro","seats":5}
{"user":"ben","plan":"free","seats":1}
Each array element became one compact line. That output drops straight into a BigQuery load job or a bulk import dialog. The tool also counts lines and objects, so you can confirm "2 objects, 2 lines" matches what you expected before you ship it anywhere.
The reverse direction reads one JSON value per line, collects them into a single array, and pretty-prints at compact, 2-space, or 4-space indent. That is what you want when a .ndjson log dump lands on your desk and you would rather fold and search it as structured data than squint at raw lines.
Finding the One Broken Row
The format's biggest day-to-day payoff is debugging. When you convert NDJSON back to an array, each line is parsed on its own and the tool stops at the first one that fails, showing its 1-based line number, for example Line 3 plus the parser's message. Blank lines that get skipped still count toward that number, so the line you see matches your editor.
I hit this last week with a nightly export of roughly 100,000 NDJSON lines that refused to import, with no hint about which row was bad. Instead of bisecting a six-megabyte file by hand, I pasted it in, converted to an array, and the tool stopped at Line 83,402. One row had a trailing comma left over from someone copying it out of a JSON array, the single most common NDJSON mistake. I deleted the comma, re-ran the import, and moved on. That trailing-comma trap is exactly why you should convert a whole array in one shot rather than hand-splitting it into lines and hoping every comma landed right.
When to Use Each Format
NDJSON is not always the answer. Reach for a plain JSON array, or convert back to one, when the data must be a single valid document: an API request body, a config file, or anything a strict JSON parser will load as one object. Raw NDJSON pasted into a tool that wants one JSON document will fail, because NDJSON is a sequence of documents, not a single one.
Reach for NDJSON when records flow one at a time, when files get large, when you append rather than rewrite, or when a loader explicitly asks for it. A useful habit: keep your canonical data as an array for editing and review, and generate NDJSON as the export format right before you load it.
If you also need to clean up the array side of the workflow, a dedicated JSON formatter handles indenting, validating, and minifying the document before you split it into lines. Together the two cover the round trip: format and verify the array, convert to NDJSON for the loader, then convert back when you need a single document again.
NDJSON earns its place by being boring and line-oriented. One value per line, no wrapping, append-friendly, streamable, and trivial to debug. Once your pipeline speaks it, the format stops being a chore and starts being the path of least resistance.
Made by Toolora · Updated 2026-06-13