JSON vs JSONL (JSON Lines): When to Use Each, With Real Examples

You can append a record to a JSONL file without rewriting a single existing byte, and a corrupt line costs you one record instead of the whole document. Those two properties are why fine-tuning datasets, Batch API jobs, and structured logs use JSONL instead of a plain JSON array.

This guide breaks down where each format wins, shows the same data in both shapes side by side, and walks through the real one-object-per-line files you hit as an ML engineer: OpenAI chat fine-tune records, Batch API request lines, and app logs. It also settles the JSONL vs NDJSON question.

The one structural difference that explains everything

Regular JSON wraps your records in an array. JSONL does not. Same three objects, two shapes:

JSON array:

[
  {"id": 1, "name": "a"},
  {"id": 2, "name": "b"},
  {"id": 3, "name": "c"}
]

JSONL (one object per line, no array, no commas):

{"id": 1, "name": "a"}
{"id": 2, "name": "b"}
{"id": 3, "name": "c"}

The JSON array is a single document. To read record three, a strict parser has to consume the opening bracket, the first two objects, and their commas first. To add a fourth record, you have to find the closing bracket, delete it, add a comma, append, and write the bracket back. With a multi-gigabyte file, that means rewriting the tail every time.

JSONL has no enclosing structure. Each line stands alone. Appending a record is a single write() with a trailing newline. Reading record three means reading until the third newline and stopping. That is why every system that streams, appends, or processes records independently reaches for JSONL.

When to use JSON, and when to use JSONL

Reach for a JSON array when the data is a single coherent object you load all at once: an API response, a config file, a manifest, anything a frontend will fetch() and hand straight to JSON.parse(). Arrays are also self-describing. The brackets tell a reader "this is a list," and any standard validator accepts it without special handling.

Reach for JSONL when records are independent and the file is large, growing, or streamed:

  • Append-only logs. Pino, Bunyan, and most structured loggers emit one JSON object per line so a crashing process never corrupts earlier entries.
  • ML training data. Fine-tuning files, RLHF datasets, and eval sets ship as JSONL so a pipeline can stream records without buffering the whole corpus in memory.
  • Batch jobs. Each line is one independent unit of work with its own identifier.
  • Warehouse exports. BigQuery and Snowflake read and write newline-delimited JSON precisely because it streams into a loader row by row.

The honest tradeoff: standard JSON tooling does not understand JSONL out of the box. JSON.parse(fileContents) on a multi-line JSONL file throws on the second {, because after parsing the first object the parser expects end-of-input, not another brace. Editors that pretty-print JSON also mangle it. You parse JSONL by splitting on newlines and parsing each line separately, which is exactly what the JSONL / NDJSON Viewer does so you can eyeball a file without writing that loop yourself.

JSONL vs NDJSON vs JSON Text Sequences

Three names get thrown around for the same basic idea. Here is what each means.

JSONL (JSON Lines) and NDJSON (Newline-Delimited JSON) describe the same format: one valid JSON value per line, lines separated by \n. The two specs were written separately and differ only in fine print. NDJSON's spec is slightly more explicit, spelling out that a parser must also accept a carriage return before the newline (\r\n) and may silently skip blank lines. In day-to-day work they are interchangeable, which is why this viewer bundles them as one format. If a tool's docs say NDJSON and your file says .jsonl, they will almost always read each other without complaint.

There is no official IANA media type registered as application/jsonl. The MIME type you will see in practice is application/x-ndjson, the unregistered convention used by Elasticsearch and most streaming HTTP APIs. If you are setting a Content-Type header for a streaming endpoint, that is the conventional choice.

JSON Text Sequences (RFC 7464, media type application/json-seq) is a distinct, formally standardized relative. Instead of only separating records with a newline, it prefixes each JSON value with an ASCII Record Separator byte (0x1E) and ends it with a line feed. That prefix removes a JSONL fragility: JSONL only works because every producer keeps each record on one physical line, but JSON itself permits insignificant whitespace, including newlines, between tokens. A pretty-printed record spans several lines and breaks a naive newline split. The Record Separator can never appear unescaped inside a JSON value, so json-seq delimits records unambiguously no matter how each value is formatted. In practice the simpler newline-only format won adoption because producers just emit compact one-line records. You will meet JSONL and NDJSON constantly; you will rarely meet json-seq outside specs that explicitly require it.

Real example 1: an OpenAI chat fine-tuning file

A supervised chat fine-tuning dataset is JSONL where each line is one training example: a messages array with roles. Three examples, three lines:

{"messages": [{"role": "system", "content": "You are a terse support bot."}, {"role": "user", "content": "Where is my order?"}, {"role": "assistant", "content": "Track it at the link in your confirmation email."}]}
{"messages": [{"role": "system", "content": "You are a terse support bot."}, {"role": "user", "content": "Reset my password."}, {"role": "assistant", "content": "Use the Forgot Password link on the sign-in page."}]}
{"messages": [{"role": "system", "content": "You are a terse support bot."}, {"role": "user", "content": "Cancel my subscription."}, {"role": "assistant", "content": "Open Billing, then Cancel plan. It stays active until the period ends."}]}

Note what is not here: no surrounding array, no commas between examples. Each line is a complete, independently parseable training record. The trainer streams them one at a time.

The format is unforgiving in one specific way that matters: the upload validator rejects the entire file if a single line has a JSON syntax error or a malformed structure. A trailing comma on line 4,000, a smart-quote that slipped in from a spreadsheet, a missing role key, and the whole upload bounces, often with an error that does not even name the offending line. That is the opposite of the stream-processing resilience JSONL gives you elsewhere. For a strict consumer, one bad line fails everything, so you find the bad line before you upload, not after the API tells you the file is invalid.

Validate line by line first. The JSONL / NDJSON Viewer flags which row broke and shows the exact parse error with its line number; for the role and structure checks specific to training data, the Fine-tuning JSONL Validator goes further and inspects the messages shape itself. Exact file-size and token limits change per provider and tier, so check current docs rather than trusting a number you read in a blog post.

Real example 2: an OpenAI Batch API request file

The Batch API takes a JSONL file where each line is one API request. Every line needs a custom_id (so you can match responses back to inputs), the HTTP method, the url of the endpoint, and the request body:

{"custom_id": "req-001", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "YOUR_MODEL", "messages": [{"role": "user", "content": "Summarize: the cat sat on the mat."}]}}
{"custom_id": "req-002", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "YOUR_MODEL", "messages": [{"role": "user", "content": "Summarize: the dog ran in the park."}]}}

The custom_id has to be unique across the file. The output comes back as JSONL too, in an order that may not match the input, and you join input to output on that id. This is the whole point of one-object-per-line here: a batch is a pile of independent jobs, and there is no natural "document" that would justify wrapping them in an array.

Two failure modes catch people. First, duplicate custom_id values, which make the results un-joinable. Second, a body that is valid JSON but wrong for the endpoint. The structural pass is what the viewer is for; for batch-specific checks (unique ids, required fields), build or validate the file with the OpenAI Batch JSONL Builder. Keep a real model name out of code you save for reference, since pricing and model availability move; YOUR_MODEL above is a placeholder you swap at submit time.

Real example 3: structured application logs

Production logs are where JSONL's resilience is the actual feature rather than a liability. A structured logger writes one JSON object per event:

{"ts": "2026-06-22T09:14:02Z", "level": "info", "svc": "api", "msg": "request", "path": "/v1/embed", "ms": 42}
{"ts": "2026-06-22T09:14:03Z", "level": "error", "svc": "api", "msg": "upstream timeout", "path": "/v1/embed", "ms": 30000}
{"ts": "2026-06-22T09:14:04Z", "level": "info", "svc": "api", "msg": "request", "path": "/v1/chat", "ms": 88}

If the process is killed mid-write and truncates the last line, every line before it is still valid and parseable. A log shipper tails the file and ships records one at a time. A grep on "level": "error" works on the raw bytes before you parse anything. None of that is possible with a JSON array that has to be syntactically complete to be readable at all.

This is the context where the format genuinely isolates a bad line: a stream processor can skip the broken record and keep going. The difference from the fine-tune case is the consumer, not the format. A lenient stream reader tolerates one corrupt line; a strict file validator rejects the whole upload. Same JSONL, opposite behavior, because the thing reading it made different choices.

Debugging JSONL: finding the line that broke

The recurring JSONL chore is "my pipeline rejected the file and I do not know which of 50,000 lines is bad." A whole-file parse is useless here, because JSON.parse() on the concatenated file fails at line 2 with a misleading message about the second object, not at the line that is actually malformed.

The fix is line-by-line parsing: split on newlines, parse each line in a try/catch, and record the index of any failure. That is the core of the JSONL / NDJSON Viewer. Paste or load a .jsonl, .ndjson, or .log file, hit Parse, and every row gets a pass/fail badge with its line number and the exact parse error for failures. The Show filter narrows to errors-only so you are not scrolling past thousands of good rows to find the one bad one. It runs entirely in your browser, so pasting production logs does not ship them anywhere.

Common culprits it surfaces fast: a trailing comma inside an object, an unescaped quote in a string value, a smart-quote pasted from a doc, a record accidentally split across two physical lines, or a stray [ from someone half-converting the file to an array. Blank lines are skipped rather than flagged, since trailing newlines are normal and counting them as errors would just add noise.

Once the file is clean, the Pretty button expands each row for diffing in review, and Compact collapses everything back to strict one-line-per-record before you re-upload. For very large files (multi-gigabyte), a browser tool is the wrong instrument; reach for jq instead. jq -c . file.jsonl compacts, and jq -s . file.jsonl slurps the whole thing into a single array if you do need array form.

Converting between the two

You convert in both directions constantly, so it helps to know the moves.

JSONL to JSON array: wrap the lines in brackets and join with commas. On the command line, jq -s . file.jsonl (slurp) does it. If you only need to flatten records into a table, skip the array entirely and go straight to NDJSON to CSV, which derives a header from every key it sees across all lines.

JSON array to JSONL: strip the outer brackets and put each element on its own line. jq -c '.[]' file.json emits one compact object per line, which is the canonical JSONL shape.

Tabular data to JSONL: if you start from a spreadsheet, run it through CSV to JSON and emit one object per row.

Two more tools earn their place in an ML workflow. When a JSONL file is malformed in ways a parser cannot recover from on its own, JSON Repair fixes per-line syntax. And training and eval sets accumulate duplicates fast; the JSONL Deduplicator drops repeated rows by whole-line hash or by a specific field before you waste tokens training on the same example twice. Do not ship a fine-tune file you have not validated and deduplicated line by line.

Use the tool

Skip the manual work. The companion tool runs this in your browser, with nothing uploaded.

JSONL / NDJSON Viewer

Frequently asked questions

Is JSONL the same as NDJSON?

For practical purposes, yes. Both mean one valid JSON value per line, separated by newlines. They come from two separately written specs that describe the same format; NDJSON's spec is a little more explicit (it spells out accepting a carriage return before the newline and optionally skipping blank lines), and files in one format read cleanly as the other. The JSONL / NDJSON Viewer treats them as a single format for exactly this reason.

Why can't I just open a JSONL file with JSON.parse()?

Because JSON.parse() expects one complete JSON value and a JSONL file is many. After it parses the first line's object, it expects the input to end, so it throws when it hits the second opening brace. You parse JSONL by splitting on newlines and parsing each line separately. A line-by-line viewer does this and tells you which line, if any, is malformed.

What MIME type should I use for JSONL?

There is no official IANA media type for JSONL. The conventional choice you will see in practice is application/x-ndjson, used by Elasticsearch and most streaming HTTP APIs. RFC 7464's JSON Text Sequences format is a separate, registered type (application/json-seq), but that is a different on-the-wire format using record-separator bytes, not plain newline-delimited JSON.

Does one bad line break a whole JSONL file?

It depends on the consumer, not the format. A lenient stream processor can skip a corrupt line and keep going, which is why logs and pipelines favor JSONL. But a strict validator, like the one that checks a fine-tuning or Batch API upload, rejects the entire file on a single syntax or structure error. Validate line by line before uploading so you find the bad row first.

How do I convert a JSONL file to a regular JSON array?

Wrap the lines in brackets and separate them with commas. With jq, run jq -s . file.jsonl to slurp every line into one array. To go the other way, jq -c '.[]' file.json emits one compact object per line. If your real goal is a table rather than an array, convert straight to CSV with the NDJSON to CSV tool.

Why do LLM fine-tuning and batch files use JSONL instead of an array?

Because the records are independent and the files are large. JSONL lets a trainer or batch runner stream records one at a time without loading the whole corpus into memory, and lets producers append records without rewriting the file. Each fine-tune line is one training example with a messages array; each Batch API line is one request with its own custom_id. An array would force the entire file to be parsed as a single document for no benefit.

Related tools