Skip to content
Back to Blog
By JSONConvert Team··7 min read

JSONL and NDJSON: The JSON Lines Format

JSONL (JSON Lines) is a file with one complete JSON value per line, usually one object per record. It shows up when you export logs, load data into BigQuery, call the Elasticsearch bulk API, or prepare a fine-tuning dataset for an LLM. It looks like JSON, the extension often says JSON, and JSON.parse still refuses to read it. This post covers what the format is, how JSONL and NDJSON differ (barely), how to convert between JSONL and a regular JSON array, and the parsing mistakes that show up in production.

What a JSONL file looks like

Here is a regular JSON array of two users:

And here is the same data as JSONL:

No outer brackets, no commas between records. Each line is valid JSON on its own, and the newline is the only separator. The note field still holds a newline, but it is written as the two characters \n, so the record stays on one physical line. Any JSON serializer does this for you, because raw newlines are not allowed inside JSON strings in the first place.

The rules from jsonlines.org are short:

  • The file is UTF-8.
  • Each line is one valid JSON value. Objects are the norm, but arrays and scalars are allowed.
  • Lines end with \n. A \r\n ending also works, because parsers treat the \r as trailing whitespace.
  • The recommended extension is .jsonl.

JSONL vs NDJSON vs a JSON array

JSONL and NDJSON (newline-delimited JSON) describe the same thing. Two groups wrote specs around the same time, and the differences are in wording, not in the bytes you write. If a tool asks for NDJSON, give it your .jsonl file. The one practical difference is the media type: NDJSON APIs such as Elasticsearch expect Content-Type: application/x-ndjson.

JSON arrayJSONL / NDJSON
Parse withJSON.parse, json.loadsOne parse call per line
Append a recordRewrite the closing ]Append one line
Stream without loading it allNeeds a streaming parserreadline or a for loop
One corrupt recordWhole file failsOne line fails, the rest still parse
Pretty-printingFineBreaks the format
Common usesAPI responses, configLogs, bulk loads, ML datasets
Use JSONL when records are written over time or the file gets large. Use a JSON array when a human reads it or a single API call returns it. If you are already fighting a 2 GB array, the large JSON files post shows how to turn it into JSONL with jq --stream.

Converting between JSON and JSONL

With jq, both directions are one command. -c prints compact output (one line per value), and -s reads every line into a single array:

In Node.js, write each record with JSON.stringify and no indent argument. Passing an indent such as 2 spreads the record over several lines and breaks the file:

Python is the same idea. json.dumps prints on one line by default:

ensure_ascii=False keeps characters like é readable instead of turning them into \u00e9. Both forms are valid JSON, but the readable one is easier to grep.

If you only have a few records and want to look at them, convert to an array and paste it into the JSON formatter. To go the other way and squeeze one pretty-printed record onto a single line, the JSON minifier produces exactly what a JSONL line needs.

Reading JSONL without breaking on the last line

Most JSONL bugs come from reading the file, not writing it. Here is the version almost everyone writes first:

It crashes on a correctly formed file:

The file ends with a newline, as it should, so split returns an empty string as the last element, and JSON.parse('') throws. Skip blank lines:

For a file too big to read into memory at once, stream it with readline. Memory stays flat no matter how many lines there are:

Catching errors per line is the main reason to use the format. One truncated record from a crashed writer costs you one line, not the whole file. Log the line number so you can find it.

In Python, iterating over the file object already streams line by line:

If you have pandas installed, pd.read_json("events.jsonl", lines=True) loads the whole file into a DataFrame in one call.

The errors that mean "this is JSONL"

If you hand a JSONL file to a normal JSON parser, it reads the first record, then complains about the next one:

Both point at line 2, column 1. That is the start of the second record, and it almost always means the file is JSONL, or someone concatenated JSON documents. Parse it line by line instead. The JSON validator gives you the same line and column, which is a quick way to confirm that the first line is fine and the problem is the second record.

The reverse mistake is a pretty-printed record inside a .jsonl file. The line-by-line reader gets { on its own and fails on line 1 with Expected property name or '}' in Node or Expecting property name enclosed in double quotes in Python. Fix the writer, not the reader: remove the indent argument.

Where you will meet JSONL

  • LLM fine-tuning and batch APIs. OpenAI's fine-tuning and Batch API both take .jsonl uploads, one request or training example per line. Validate every line before uploading, because the job rejects the whole file over one bad record.
  • BigQuery. Loading JSON requires NEWLINE_DELIMITED_JSON as the source format. A JSON array will not load.
  • Elasticsearch and OpenSearch _bulk. The body is NDJSON, alternating action lines and document lines, and it must end with a newline. Leaving off the final newline is a classic cause of a 400 error.
  • Structured logs. Most JSON loggers write one object per line, so grep, tail -f, and wc -l all work on the file directly.

Getting JSONL into a spreadsheet

JSONL has no direct path to CSV in most tools, so go through an array. Run jq -s '.' events.jsonl, then paste the result into the JSON to CSV converter, which flattens nested fields into dot-notation columns. Going the other way, the CSV to JSON converter gives you an array, and jq -c '.[]' turns that into JSONL.

One last tip. Always end the file with a newline. Without one, cat a.jsonl b.jsonl joins the last record of the first file and the first record of the second into a single broken line, and you will not notice until something tries to parse it.

Related Tools