Free online JSON formatter, validator, and developer tools.
A standard JSON array requires the whole document to be valid before you can safely read any of it โ one malformed record at the end, or a process that dies mid-write, and the entire file becomes unparseable. JSON Lines (also written JSONL, or newline-delimited JSON / NDJSON) fixes this by dropping the wrapping array entirely: each line is its own complete, independently parseable JSON value.
A JSONL file is plain text where every line is a self-contained JSON value, almost always an object, separated by a single newline character (\n). There is no wrapping [ ], and no commas between records:
{"timestamp": "2026-07-12T09:00:00Z", "level": "info", "message": "Server started"}
{"timestamp": "2026-07-12T09:00:03Z", "level": "warn", "message": "High memory usage", "pct": 87}
{"timestamp": "2026-07-12T09:00:07Z", "level": "error", "message": "Connection timeout", "host": "db-2"}
Each line above is independently valid JSON. That single property โ line-level independence โ is what standard JSON arrays lack and what makes JSONL suited to a different class of problem.
Compare the same data as a standard JSON array:
[
{"timestamp": "2026-07-12T09:00:00Z", "level": "info", "message": "Server started"},
{"timestamp": "2026-07-12T09:00:03Z", "level": "warn", "message": "High memory usage", "pct": 87},
{"timestamp": "2026-07-12T09:00:07Z", "level": "error", "message": "Connection timeout", "host": "db-2"}
]
Three practical problems emerge once this file grows to gigabytes or is written incrementally:
JSON.parse() needs the entire file in memory and requires the trailing ] to exist โ a process that crashes mid-write leaves an unparseable file, because the array was never closed.], inserting a comma, writing the new object, and re-closing the array โ not a simple append. With JSONL, appending a new record is just writing one more line to the end of the file.grep, wc -l, tail -f, and `split` all assume line-oriented data. A pretty-printed JSON array spans arbitrary numbers of lines per record with no reliable boundary; JSONL's boundary is always exactly one line.JSONL solves all three by making every record independently addressable and appendable.
jq, Spark, and BigQuery load JSONL natively because each line maps cleanly to one rowNo special library is required for basic use โ plain line iteration plus json.loads/json.dumps is often enough:
import json
# Reading โ stream line by line, never load the whole file into memory
def read_jsonl(path):
with open(path, 'r') as f:
for line in f:
line = line.strip()
if line:
yield json.loads(line)
for record in read_jsonl('logs.jsonl'):
print(record['level'], record['message'])
# Writing โ append one record at a time
def append_jsonl(path, record):
with open(path, 'a') as f:
f.write(json.dumps(record) + '\n')
append_jsonl('logs.jsonl', {"timestamp": "2026-07-12T09:00:10Z", "level": "info", "message": "Request handled"})
For larger workloads, the jsonlines package wraps this pattern with a slightly cleaner API and built-in error handling for malformed lines:
pip install jsonlines
import jsonlines
with jsonlines.open('logs.jsonl') as reader:
for obj in reader:
print(obj)
with jsonlines.open('logs.jsonl', mode='a') as writer:
writer.write({"timestamp": "2026-07-12T09:00:12Z", "level": "info", "message": "Done"})
For small-to-medium files, splitting on newlines is sufficient:
const fs = require('fs');
// Reading
const lines = fs.readFileSync('logs.jsonl', 'utf8').split('\n').filter(Boolean);
const records = lines.map(line => JSON.parse(line));
// Writing / appending
function appendJsonl(path, record) {
fs.appendFileSync(path, JSON.stringify(record) + '\n');
}
appendJsonl('logs.jsonl', { timestamp: new Date().toISOString(), level: 'info', message: 'Request handled' });
For large files where you cannot hold the whole thing in memory, stream it line-by-line with Node's readline module:
const fs = require('fs');
const readline = require('readline');
async function processLargeJsonl(path) {
const rl = readline.createInterface({
input: fs.createReadStream(path),
crlfDelay: Infinity,
});
for await (const line of rl) {
if (!line.trim()) continue;
const record = JSON.parse(line);
// process record here, one at a time โ constant memory usage
}
}
jq processes JSONL natively โ pass one record through the filter for each line:
# Filter to only error-level log lines
cat logs.jsonl | jq 'select(.level == "error")'
# Extract just the message field from every line
cat logs.jsonl | jq -r '.message'
# Convert a JSON array file into JSONL
jq -c '.[]' data.json > data.jsonl
# Convert JSONL back into a JSON array
jq -s '.' data.jsonl > data.json
Standard Unix tools also work directly because the format is line-oriented: wc -l logs.jsonl counts records, tail -f logs.jsonl tails a live-growing log, and split -l 100000 logs.jsonl chunk_ splits a huge file into fixed-size chunks without ever parsing a single record.
Use a standard JSON array when the data is a single cohesive document that's always read and written as a whole โ an API response, a config file, a small dataset that comfortably fits in memory. Use JSONL when records are generated incrementally over time, need to be processed one at a time without loading everything into memory, or need to survive partial writes gracefully โ logs, event streams, and large training datasets are the classic cases. If you're unsure, ask whether the file is ever appended to after creation; if yes, JSONL is almost always the better fit.
JSON Lines trades the single-document guarantees of a JSON array for line-level independence: constant-memory streaming reads, safe appends, and compatibility with line-oriented Unix tooling. It's not a replacement for JSON arrays in general โ it's the right specialized format specifically for logs, streams, and large sequential datasets where records arrive or need to be processed one at a time.
Need to inspect a single record from a JSONL file? Paste just that line into our free JSON Formatter to pretty-print and validate it.