JSON vs JSONL (JSON Lines): Differences, Examples, and When to Use Each

Quick answer

JSON is one document. JSONL (JSON Lines) is one JSON object per line. Use JSON for APIs and config files. Use JSONL for logs, streaming data, large datasets, and any pipeline where loading an entire JSON array into memory is not practical.

What is JSONL?

JSONL (JSON Lines), also called NDJSON (Newline-Delimited JSON), is a text format where every line is a complete, independently valid JSON value — most commonly a JSON object. The file extension is .jsonl or .ndjson. Each line is terminated by a newline (\n).

JSONL is not valid JSON. You cannot paste a .jsonl file into a JSON validator and expect it to pass — it will fail because the file is not a single JSON document. Each individual line is valid JSON; the file as a whole is not.

JSONL vs NDJSON: are they the same?

Yes — JSONL and NDJSON are the same format with different names:

Both specifications define the same rule: one complete JSON value per line, lines separated by \n. The file extensions .jsonl and .ndjson are interchangeable. Tools like Apache Spark, BigQuery, and jq accept both names for the same thing. The difference is purely naming convention — use whichever the tool or API you are working with expects.

A third alias you may encounter is JSON-seq (RFC 7464), which is slightly different — it prefixes each record with a Unicode Record Separator byte. JSON-seq is used by some log formats but is less common than JSONL/NDJSON in practice.

Visual: JSON array vs JSONL (line-by-line)

The core difference in how each format is processed:

JSON (array)                     JSONL / NDJSON
─────────────────────────        ─────────────────────────
[                                {"id":1,"event":"login"}
  {"id":1,"event":"login"},      {"id":2,"event":"buy"}
  {"id":2,"event":"buy"},        {"id":3,"event":"logout"}
  {"id":3,"event":"logout"}
]
         │                                │
         ▼                                ▼
 Must load ENTIRE file            Read ONE line at a time
 into memory before               → constant memory
 first record is readable         → append = one write
                                  → bad line = skip it

Side-by-side: the same data in JSON and JSONL

Standard JSON (array of objects)

[
  { "id": 1, "event": "login",  "user": "alice", "ts": "2026-06-11T09:00:00Z" },
  { "id": 2, "event": "purchase", "user": "bob",   "ts": "2026-06-11T09:01:12Z" },
  { "id": 3, "event": "logout", "user": "alice", "ts": "2026-06-11T09:05:44Z" }
]

To read record 2, the parser must load the entire array. To append record 4, you must read the file, parse the JSON, push to the array, and rewrite the whole file — O(n) for every write.

JSONL (one object per line)

{"id":1,"event":"login","user":"alice","ts":"2026-06-11T09:00:00Z"}
{"id":2,"event":"purchase","user":"bob","ts":"2026-06-11T09:01:12Z"}
{"id":3,"event":"logout","user":"alice","ts":"2026-06-11T09:05:44Z"}

To append record 4, open the file in append mode and write one line — O(1). To process 10 million records, read one line at a time — constant memory.

Quick comparison

FeatureJSONJSONL
StructureSingle document (object or array)One JSON value per line
Valid JSONYesNo (per file; each line is)
File extension.json.jsonl, .ndjson
StreamingNot supportedNative — read line by line
Memory to readFull file must be parsedOne line at a time
Append a recordMust rewrite the fileAppend one line, O(1)
Human readabilityPretty-printable, clear structureHarder to read at scale
Typical use casesAPIs, config, small datasetsLogs, ML datasets, ETL pipelines
Tool supportUniversalSupported by Spark, BigQuery, Elasticsearch, jq
Error isolationOne bad byte breaks the whole fileOne bad line, rest still parseable

The memory problem with large JSON arrays

A standard JSON array must be fully loaded and parsed before you can access a single record. This is fine for a 50 KB API response. It is a serious problem for a 10 GB log file or a 100 million record ML training set.

Consider an event log that grows by 1 million rows per day. Storing it as a JSON array means:

JSONL solves all three: memory is bounded to the size of the largest single line, append is O(1), and a corrupt line only invalidates that one record.

Reading and writing JSONL in Python

The standard json module handles JSONL with a simple loop. If you prefer a cleaner API, the jsonlines package on PyPI wraps the loop and handles edge cases automatically.

import json

# Read JSONL — one record at a time, constant memory
with open('events.jsonl', 'r') as f:
    for line in f:
        line = line.strip()
        if line:
            record = json.loads(line)
            print(record['event'], record['user'])

# Write JSONL
records = [
    {"id": 1, "event": "login",  "user": "alice"},
    {"id": 2, "event": "purchase", "user": "bob"},
]
with open('events.jsonl', 'w') as f:
    for record in records:
        f.write(json.dumps(record) + '\n')

# Append a single record without touching the rest of the file
new_event = {"id": 3, "event": "logout", "user": "alice"}
with open('events.jsonl', 'a') as f:
    f.write(json.dumps(new_event) + '\n')

Reading JSONL in Node.js

Node’s built-in readline module is all you need. For a higher-level API, the ndjson package on npm provides a Transform stream that emits parsed objects directly.

const fs = require('fs');
const readline = require('readline');

async function readJsonl(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({ input: fileStream });

  for await (const line of rl) {
    if (line.trim()) {
      const record = JSON.parse(line);
      console.log(record.event, record.user);
    }
  }
}

readJsonl('events.jsonl');

// Write JSONL
const records = [
  { id: 1, event: 'login',    user: 'alice' },
  { id: 2, event: 'purchase', user: 'bob'   },
];
const output = records.map(r => JSON.stringify(r)).join('\n') + '\n';
fs.writeFileSync('events.jsonl', output);

Convenience libraries

The standard-library approaches above are sufficient for most use cases. For cleaner code or extra features, these well-maintained packages are worth knowing:

# Python: jsonlines package
# pip install jsonlines
import jsonlines

# Read
with jsonlines.open('events.jsonl') as reader:
    for obj in reader:
        print(obj['event'])

# Write
with jsonlines.open('out.jsonl', mode='w') as writer:
    writer.write_all([
        {"id": 1, "event": "login"},
        {"id": 2, "event": "buy"},
    ])
// Node.js: ndjson package
// npm install ndjson
const fs = require('fs');
const ndjson = require('ndjson');

// Read — emits parsed objects as a stream
fs.createReadStream('events.jsonl')
  .pipe(ndjson.parse())
  .on('data', obj => console.log(obj.event));

// Write — serializes objects to NDJSON/JSONL
const serialize = ndjson.stringify();
serialize.pipe(fs.createWriteStream('out.jsonl'));
serialize.write({ id: 1, event: 'login' });
serialize.write({ id: 2, event: 'buy' });
serialize.end();

Converting between JSON and JSONL with jq

jq is the standard command-line tool for JSON processing. It handles JSONL natively when you use the -c (compact) flag.

# JSON array → JSONL
jq -c '.[]' data.json > data.jsonl

# JSONL → JSON array
jq -s '.' data.jsonl > data.json

# Process JSONL: extract one field from each record
jq -r '.user' events.jsonl

# Filter JSONL: only purchase events
jq -c 'select(.event == "purchase")' events.jsonl

# Count records in a JSONL file (no need to parse the whole thing)
wc -l events.jsonl

Streaming JSONL from an HTTP API

JSONL is the format of choice for streaming API responses — the server writes one JSON object per line as data becomes available, and the client reads and processes each line as it arrives without waiting for the complete response. Many AI APIs (including LLM completion endpoints) use a conceptually similar incremental format, where data arrives as a stream of JSON objects rather than a single document. The pattern for consuming them in the browser is the same: buffer chunks, split on newlines, parse each line.

// Consume a JSONL streaming response in the browser
const response = await fetch('/api/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop(); // keep incomplete last line

  for (const line of lines) {
    if (line.trim()) {
      const record = JSON.parse(line);
      console.log('Received:', record);
    }
  }
}

Where JSONL is the standard

When to stick with JSON

Error resilience

One underrated advantage of JSONL is error isolation. If a JSON array file has a syntax error near the start — a stray comma, a missing quote — the entire file fails to parse. Nothing is recoverable.

In a JSONL file, a bad line only corrupts that one record. A parser that skips lines with json.JSONDecodeError can recover and process the remaining millions of records. For long-running log files written by multiple processes, this resilience is practically important.

Frequently Asked Questions

What is JSONL (JSON Lines)?

JSONL (JSON Lines), also called NDJSON (Newline-Delimited JSON), is a text format where each line contains a single, complete, valid JSON value — typically a JSON object. Files use the .jsonl or .ndjson extension. JSONL is not valid JSON itself; it is a separate streaming-friendly format that happens to use JSON syntax for each line.

Why use JSONL instead of a JSON array?

JSON arrays must be fully loaded into memory before any record can be read. A 10 GB JSON array requires ~10 GB of memory. JSONL lets you read and process one line at a time — constant memory regardless of file size. JSONL also supports appending without rewriting the file: open in append mode, write one line. With a JSON array, you must parse, modify, and rewrite the whole file.

What is the difference between JSONL and NDJSON?

JSONL and NDJSON are the same format with different names. Both store one JSON value per line separated by newlines. NDJSON stands for Newline-Delimited JSON. The .jsonl and .ndjson extensions are interchangeable. The JSONL specification and the NDJSON specification describe functionally identical formats.

Can I use the JSON formatter with JSONL files?

Not directly — JSONL is not valid JSON, so a standard JSON formatter will reject the whole file. To inspect a JSONL file, paste a single line from the file into the JSON Formatter. Each line is independently valid JSON and will format and validate correctly.

Is JSONL supported by big data tools?

Yes. JSONL is natively supported by Apache Spark (read via spark.read.json()), BigQuery (newline-delimited JSON import), Elasticsearch (bulk API), Hugging Face datasets, and jq which works line by line on JSONL streams without modification.

How do I convert a JSON array to JSONL?

With Python: import json; [open('out.jsonl','a').write(json.dumps(r)+'\n') for r in json.load(open('in.json'))]. With jq: jq -c '.[]' data.json > data.jsonl. The -c flag outputs compact single-line JSON; .[] iterates over each array element.

Need to inspect a JSONL record?

Paste any single line from a .jsonl file into the formatter — each line is valid JSON and will pretty-print cleanly.

JSON Formatter JSON Validator
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.