JSON vs JSONL: What’s the Difference, and When to Use Each
JSON stores one value per file, usually one big object or array. JSONL (JSON Lines) stores one JSON object per line, separated by newlines…
JSON vs JSONL: What’s the Difference, and When to Use Each
JSON stores one value per file, usually one big object or array. JSONL (JSON Lines) stores one JSON object per line, separated by newlines. JSON has to be read all at once. JSONL can be read, written, and streamed one line at a time. That is the whole difference, and it is why log files, event streams, and data pipelines use JSONL instead of plain JSON.
This post covers what the two formats are, how they differ, what the speed and memory difference actually is (I measured it), the mistakes that break JSONL, how to convert and read it in a few languages, and where each one fits. If you came here because Claude Code keeps writing .jsonl files to your disk, I wrote a separate post on why Claude Code uses JSONL and how to read those files.
Photo by Iván Díaz on Unsplash
What is JSON?
JSON is a single document. A list of records looks like this:
[
{ "id": 1, "event": "start" },
{ "id": 2, "event": "stop" }
]
The file is one array. It is only valid once the closing ] is written. To read it, a parser has to load the whole file into memory and parse it in one pass (JSON.parse in JavaScript, json_decode in PHP, json.load in Python).
This is fine for config files and API responses. It becomes a problem when the data is large or still being written.
What is JSONL?
JSONL (JSON Lines) is the same data with one object per line and no wrapping array:
{ "id": 1, "event": "start" }
{ "id": 2, "event": "stop" }
Each line is a complete, valid JSON object. Lines are separated by \n. There is no array around them and no commas between them.
The file extension is .jsonl. You will also see .ndjson (Newline Delimited JSON). It is the same format under a different name.
Because every line stands on its own, you can:
- read one line, process it, and move to the next without loading the whole file
- add a new record by appending one line to the end
- start processing while the file is still being written
JSON vs JSONL: Side by Side
Same three records, both formats:
JSON
[
{ "type": "user", "text": "fix the test" },
{ "type": "assistant", "text": "reading the file" },
{ "type": "result", "cost": 0.04 }
]
JSONL
{ "type": "user", "text": "fix the test" }
{ "type": "assistant", "text": "reading the file" }
{ "type": "result", "cost": 0.04 }
JSON vs JSONL: Comparison

JSONL vs JSON comparison table
- File structure — JSON: one object or array. JSONL: one object per line.
- One line valid on its own? — JSON: no. JSONL: yes.
- Whole file valid JSON? — JSON: yes. JSONL: no.
- Reading — JSON: load and parse the whole file. JSONL: read line by line.
- Memory use — JSON: grows with file size. JSONL: constant, one line at a time.
- Appending a record — JSON: rewrite the file. JSONL: append one line.
- Streaming — JSON: hard, you need the whole file first. JSONL: built for it.
- Random access — JSON: yes, after parsing. JSONL: no, you scan from the top.
- Top-level metadata — JSON: easy, wrap records in an object. JSONL: no envelope.
- Crash safety — JSON: a broken write can ruin the file. JSONL: you lose at most the last line.
-
- Best for — JSON: configs, API responses, small data. JSONL: logs, events, large or streamed data.
Four of those rows do most of the work: appending, streaming, crash safety, and memory use. Any time data arrives over time instead of all at once, those four are the reason to pick JSONL.
I Measured It: JSON vs JSONL on 200,000 Records
People say “JSONL is faster” but rarely show numbers, and the truth is more mixed. So I ran a test. It makes 200,000 small records (about 34 MB), writes them both ways, and measures size, speed, and memory. Here is what came back, on Python 3.14, Apple Silicon:

JSONL vs JSON benchmarks
- Raw file size — JSON: 33.9 MB. JSONL: 33.7 MB.
- Gzipped size — JSON: 1.81 MB. JSONL: 1.80 MB.
- Write the file — JSON: 637 ms. JSONL: 267 ms.
- Read and parse the whole file — JSON: 554 ms. JSONL: 835 ms.
- Peak memory to read it — JSON: 148 MB. JSONL: 0.15 MB.
- Reach the first record — JSON: 141 ms. JSONL: 0.1 ms.
-
- Append one record — JSON: 747 ms. JSONL: 0.1 ms.
Two of those rows surprise people:
- File size is basically the same. JSON and JSONL hold the same keys and values. JSONL drops the array brackets and commas but adds a newline per line, so it comes out a little smaller. After gzip the difference is gone. The idea that “JSONL wastes space” is not true when you compare it to a JSON array.
- For reading the whole file into memory, JSON is actually faster. One big
json.loadis a single trip into the C parser. JSONL makes one small parse per line, and that overhead adds up. So if you always need every record in memory at once, plain JSON is the simpler and slightly faster choice. - For everything else, JSONL wins by a lot. It uses about a thousand times less memory, because it holds one line at a time instead of the whole file. It reaches the first record almost instantly instead of parsing 34 MB first. And appending one record is a single write to the end of the file, instead of loading the whole array, adding to it, and writing it all back.
The last row matters most. Appending to JSON took 747 ms because the whole file has to be read, changed, and written again. JSONL took a tenth of a millisecond, because you open the file and write one line. In a log that writes thousands of times a second, that gap is the reason to use JSONL.
Here is the script if you want to run it yourself:
import json, gzip, os, time, tracemalloc
N = 200_000
records = [{"id": i, "level": "info", "path": f"/api/items/{i % 100}",
"duration_ms": (i * 7) % 900, "ok": i % 5 != 0} for i in range(N)]
# write JSON (one array) vs JSONL (one object per line)
with open("bench.json", "w") as f:
json.dump(records, f)
with open("bench.jsonl", "w") as f:
for r in records:
f.write(json.dumps(r) + "\n")
# read the whole JSON file: holds everything in memory
tracemalloc.start()
with open("bench.json") as f:
data = json.load(f)
print("JSON peak MB:", tracemalloc.get_traced_memory()[1] / 1e6)
tracemalloc.stop()
# read JSONL line by line: holds one line in memory
tracemalloc.start()
with open("bench.jsonl") as f:
for line in f:
row = json.loads(line)
print("JSONL peak MB:", tracemalloc.get_traced_memory()[1] / 1e6)
When to Use JSON
Use plain JSON when:
- It is a config file (
package.json,tsconfig.json) - It is an API response you build and return all at once
- The data is one nested structure you want to load as a single object
- The file is small (under a few MB) and you always need all of it
- You need to jump straight to a record by key without scanning
When to Use JSONL
Use JSONL when:
- It is a log or event stream that only grows
- You append records often
- The file can get large and you do not want to load it all into memory
- You want to process data while it is still being written
- It is a dataset for machine learning (JSONL is the standard for training and fine-tuning data)
A quick way to decide: if your data arrives over time, use JSONL. If you have all of it at once and always need the whole thing, use JSON.
The Mistakes That Break JSONL
JSONL has one rule that is easy to break, plus a few smaller traps. These are the ones that cost people an hour.
Each object must be on one line. This is the big one. JSONL means one JSON value per physical line. If you pretty-print your objects across several lines, the file is no longer JSONL and a line reader will choke on the first { with nothing after it.
# correct: one object, one line
{"id": 1, "event": "start"}
# broken: one object spread over many lines
{
"id": 1,
"event": "start"
}
In Python, json.dumps(obj) gives you one line, which is what you want. json.dumps(obj, indent=2) does not. With jq, plain jq . pretty-prints, so use jq -c (compact) to keep one object per line.
End each line with a newline, including the last one. Most readers handle a missing final newline, but appending later is cleaner when every line already ends in \n.
Skip blank lines when reading. Streamed or hand-edited files pick up empty lines. Skip them so a blank line does not throw a parse error.
Watch line endings and BOM. On Windows you can end up with \r\n. Read in text mode so the \r is handled for you. Write UTF-8 with no byte order mark, or the mark sits at the front of the first line and breaks the first parse.
A safe reader skips the junk instead of crashing:
import json
with open("data.jsonl", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue # skip blank lines
try:
row = json.loads(line)
except json.JSONDecodeError:
continue # skip a half-written or bad line
...
Converting Between JSON and JSONL
The fastest way is jq.
JSON array to JSONL (one line per element):
jq -c '.[]' data.json > data.jsonl
JSONL back to a JSON array:
jq -s '.' data.jsonl > data.json
The -c flag keeps each object on one line. The -s (slurp) flag reads all the lines and wraps them in one array.
In Python:
import json
# array -> jsonl
with open("data.json") as f:
rows = json.load(f)
with open("data.jsonl", "w") as f:
for row in rows:
f.write(json.dumps(row) + "\n")
# jsonl -> array
rows = [json.loads(line) for line in open("data.jsonl") if line.strip()]
json.dump(rows, open("data.json", "w"))
With pandas it is one call each way:
import pandas as pd
df = pd.read_json("data.jsonl", lines=True) # read JSONL
df.to_json("out.jsonl", orient="records", lines=True) # write JSONL
Reading and Writing JSONL in Different Languages
The pattern is the same everywhere: loop over lines, parse each one, and append by writing a single line.
Python
import json
# read
with open("data.jsonl") as f:
for line in f:
row = json.loads(line)
# append one record
with open("data.jsonl", "a") as f:
f.write(json.dumps({"id": 99, "event": "new"}) + "\n")
Node.js
import { createReadStream, appendFileSync } from "node:fs";
import { createInterface } from "node:readline";
// read
const rl = createInterface({ input: createReadStream("data.jsonl") });
for await (const line of rl) {
if (line.trim()) {
const row = JSON.parse(line);
}
}
// append one record
appendFileSync("data.jsonl", JSON.stringify({ id: 99, event: "new" }) + "\n");
Go
f, _ := os.Open("data.jsonl")
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 16*1024*1024) // raise the line limit for big lines
for sc.Scan() {
var row map[string]any
json.Unmarshal(sc.Bytes(), &row)
}
One Go gotcha: bufio.Scanner has a default max line size of 64 KB and will stop silently on a longer line. Raise the buffer as shown if your records can be big.
Command line with jq
# only the error lines, still as JSONL
jq -c 'select(.level == "error")' data.jsonl
# pull one field out as plain text
jq -r '.path' data.jsonl
The Limitations of JSONL
JSONL has downsides too. The line-by-line design that makes it good for streams also costs you a few things.
- The whole file is not valid JSON. You cannot hand a
.jsonlfile to a parser that expects one document. Tools that assume.jsonmay reject it. - No random access. To read record 500,000 you scan from the top, because nothing tells you where that line starts. If you need fast lookups by key, JSONL alone is the wrong tool. People solve this by building a side index, splitting the file, or loading it into a database.
- No place for top-level metadata. A JSON document can wrap records in an envelope like
{"count": 200000, "records": [...]}. JSONL has no envelope. Metadata has to go on its own line or in a separate file. - No comments. Same as JSON. There is nowhere to leave a note.
- Bad for small, whole-file data. For a config or a small object you always read in full, the array brackets cost you nothing and plain JSON is simpler.
JSONL for Machine Learning and LLM Fine-Tuning
If you have trained or fine-tuned a model, you have already used JSONL. OpenAI and Anthropic both take fine-tuning data as JSONL, one training example per line:
{"messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"Translate hello to French."},{"role":"assistant","content":"Bonjour."}]}
{"messages":[{"role":"user","content":"Capital of Japan?"},{"role":"assistant","content":"Tokyo."}]}
The format fits the job. Training sets are huge, they are read once, and the order does not matter much. A line reader streams the examples without ever loading the whole set into memory, and you can append new examples to the end without touching the rest. The same goes for prediction inputs and outputs in batch jobs: one record per line, in and out.
Why Big Data Tools Love JSONL
Spark, Hadoop, DuckDB, and BigQuery all use newline-delimited JSON for one main reason: you can split it.
A JSONL file can be cut at any newline. You can hand one worker the bytes from 0 to 100 MB and another worker the bytes from 100 MB to 200 MB, and each one finds whole records by scanning to the next newline. A single JSON array cannot be split this way, because a chunk from the middle is not valid on its own. There is no safe place to cut it.
That is what lets these tools spread one big file across many machines and work on it at the same time. In practice:
- DuckDB reads
.ndjsondirectly withread_json_auto. - BigQuery imports newline-delimited JSON as a load format.
- pandas reads it with
read_json(path, lines=True), and Polars withread_ndjson. - Spark and Hadoop treat each line as one record and split the file across workers.
JSONL vs CSV vs Parquet
JSONL is not the only row format. Here is where it sits.
- vs CSV. CSV is smaller and every tool reads it, but it is flat. No nested objects or arrays, every value is a string until you cast it, and quoting gets messy once your data has commas or newlines in it. JSONL keeps nesting and real types. Use CSV for simple tables. Use JSONL when records have structure.
- vs Parquet. Parquet is columnar and compressed. For analytics over huge datasets, where you scan a few columns out of many, it is far smaller and faster than JSONL. But it is a binary format you cannot read by eye or append a line to. A common setup is to save data as JSONL first, since it is easy to write and append, then convert it to Parquet for heavy queries.
The Specs: JSON Lines, NDJSON, and JSON-seq
These are the same idea in practice. It still helps to know the names.
- JSON Lines (jsonlines.org): one JSON value per line, UTF-8, lines separated by
\n. This is the.jsonlyou see most often. - NDJSON (Newline Delimited JSON): the same format under a different name. You see it in logging tools and as the
.ndjsonextension. JSONL and NDJSON are interchangeable. - JSON Text Sequences (RFC 7464): a stricter cousin that puts a record separator byte (
0x1E) before each value. You rarely need it, but it exists for streams where a bare newline inside data could cause trouble.
If someone hands you a .jsonl or a .ndjson file, treat them the same way.
Tools That Speak JSONL
You do not need to write a parser. These read JSONL out of the box:
- Command line:
jqand Miller (mlr) for filtering and reshaping,fxandjlessfor browsing. - Databases and queries: DuckDB, ClickHouse, BigQuery.
- Python: pandas (
read_json(lines=True)), Polars (read_ndjson), or a plain file loop. - Logs: most structured loggers can write one JSON object per line, which you then
tail -fand pipe throughjq.
Frequently Asked Questions
Is JSONL valid JSON? The whole file is not valid JSON, but each line is. You parse it line by line, not as one document.
What is the file extension for JSONL? .jsonl. You will also see .ndjson, which is the same format.
What is the difference between JSONL and NDJSON? There is no real difference. JSON Lines and Newline Delimited JSON describe the same thing: one JSON object per line. The names are used interchangeably.
Is JSONL faster than JSON? It depends on what you are doing. For appending records, reaching the first record, and keeping memory low, JSONL is far faster. For reading an entire file into memory in one go, plain JSON is actually a little faster, because it is one parse instead of many. See the benchmark above.
Is a JSONL file bigger than a JSON array? No. They hold the same keys and values, so the sizes are nearly identical, and after gzip the difference disappears.
Can JSONL hold nested objects? Yes. Each line is a full JSON value, so it can be as nested as you like, as long as the whole object stays on one line.
How do I read line N without reading the whole file? You cannot jump straight to it. JSONL has no index, so you scan from the top, or you split the file, or you load it into a database that does support lookups.
Can you stream JSON? Not easily. A JSON array is not valid until the closing bracket arrives, so you have to buffer the whole thing. JSONL is built for streaming because each line is complete on its own.
How do you read a JSONL file? Read it line by line and parse each line as JSON. In most languages that is a loop over the file handle. Do not load the whole file and run a single parse on it.
How do you convert between JSON and JSONL? With jq: a JSON array to JSONL is jq -c '.[]' data.json > data.jsonl. JSONL back to a JSON array is jq -s '.' data.jsonl > data.json.
Summary
JSON and JSONL hold the same kind of data. The difference is structure. JSON is one document you read all at once. JSONL is one object per line that you can read, append, and stream a line at a time.
The numbers back it up. For appending and for keeping memory low, JSONL beats JSON by a thousand times or more. For reading a whole file in one shot, plain JSON is a little faster and a lot simpler. So use JSON for configs and API responses, and use JSONL for logs, event streams, large datasets, machine learning data, and anything written or read over time.
For a real example of JSONL in the wild, see how Claude Code stores every session as a JSONL file, and why that one choice makes the tool work the way it does.
메타데이터
- post_id
- 1cb7149208fa
- slug
- jsonl-vs-json-1cb7149208fa
- url
- https://medium.com/@_suleyman/jsonl-vs-json-1cb7149208fa
- canonical_url
- https://medium.com/@_suleyman/jsonl-vs-json-1cb7149208fa
- author_url
- https://medium.com/@_suleyman
- status
- ok
- fetched_at
- 2026-07-14 13:28:58