You export logs from a data warehouse, or download a training dataset, or pull events from an analytics platform. You open the file and see this:
{"id": 1, "event": "login", "ts": "2026-08-01T09:12:00Z"}
{"id": 2, "event": "purchase", "ts": "2026-08-01T09:14:22Z"}
{"id": 3, "event": "logout", "ts": "2026-08-01T09:31:07Z"} Then you feed it to a JSON tool and get Unexpected token { in JSON at position 52, or in Python, ValueError: Extra data.
Nothing is corrupt. This is JSON Lines — also called JSONL or NDJSON (newline-delimited JSON). Each line is a complete, valid JSON object. The file as a whole is not valid JSON, because there is no array wrapping the objects and no commas between them.
Why this format exists
It solves a real problem. With a regular JSON array, you cannot process the first record until you have read enough of the file to know the structure is sound, and you cannot append a record without rewriting the closing bracket. JSON Lines fixes both:
- Streamable. Read one line, parse it, process it, discard it. Memory stays flat whether the file is 1 MB or 100 GB.
- Appendable. Adding a record means writing one more line. No rewriting.
- Resilient. One malformed line does not invalidate the rest of the file.
- Splittable. Cut the file at any newline and both halves are still valid.
That is why you find it in BigQuery and Redshift exports, machine learning datasets, log pipelines, and the OpenAI fine-tuning format.
How to tell instantly: Look at the first character of the file. If it is [, you have regular JSON. If it is { and there is more than one line, you almost certainly have JSON Lines. Common extensions are .jsonl, .ndjson, and — confusingly — plain .json.
Method 1: Python with pandas (one line)
pandas has direct support. The whole conversion is:
import pandas as pd
df = pd.read_json("events.jsonl", lines=True)
df.to_excel("events.xlsx", index=False)lines=True is the entire trick. Without it you get ValueError: Trailing data, which is the error message that brings most people to this page.
If the records contain nested objects, normalize them first:
import json
import pandas as pd
records = []
with open("events.jsonl", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line: # skip blank lines
records.append(json.loads(line))
df = pd.json_normalize(records)
df.to_excel("events.xlsx", index=False) The if line check matters more than it looks. Files exported from pipelines frequently end with a trailing newline, and json.loads("") raises JSONDecodeError: Expecting value: line 1 column 1 (char 0). That single blank line at the end of the file is a remarkably common cause of failed conversions.
Method 2: Convert JSONL into regular JSON
If you want to use a standard converter — including browser-based ones — turn the file into a proper JSON array first. Structurally, you need to wrap everything in brackets and put commas between the lines.
With jq
jq -s '.' events.jsonl > events.json-s (slurp) reads the whole input stream and collects it into a single array. This is the shortest reliable method if you have jq installed.
With Python
import json
with open("events.jsonl", encoding="utf-8") as f:
records = [json.loads(line) for line in f if line.strip()]
with open("events.json", "w", encoding="utf-8") as f:
json.dump(records, f, indent=2)By hand, for small files
For a file of a few dozen lines you can do this in any text editor that supports regular expression replace:
- Replace
\n(newline) with,\nto put commas between records. - Remove the trailing comma on the last line.
- Add
[at the very top and]at the very bottom.
The result is standard JSON, and you can drop it into our JSON to Excel converter directly.
Do not do this for large files. The whole reason JSONL exists is to avoid holding everything in memory at once. Wrapping a 2 GB JSONL file into an array recreates exactly the problem the format was designed to prevent. Above roughly 50 MB, stream it instead — see working with large JSON files.
Method 3: Streaming straight to Excel
For large JSONL files, read line by line and write rows as you go. Memory stays flat:
import json
from openpyxl import Workbook
wb = Workbook(write_only=True)
ws = wb.create_sheet("Events")
headers = None
with open("events.jsonl", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
record = json.loads(line)
if headers is None:
headers = list(record.keys())
ws.append(headers)
ws.append([record.get(h) for h in headers])
wb.save("events.xlsx")write_only=True is what keeps openpyxl from building the entire worksheet in memory before saving.
Handling ragged records
JSONL files are often ragged — later records carry fields the earlier ones did not, because the producing system added a field partway through. Taking headers from the first record silently drops those columns.
If you suspect this, collect the full set of keys in one pass first:
import json
keys = []
seen = set()
with open("events.jsonl", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
for k in json.loads(line):
if k not in seen:
seen.add(k)
keys.append(k) # preserves first-seen order
print(f"{len(keys)} distinct fields: {keys}") Then use keys as your headers on a second pass. pandas does this automatically — read_json(..., lines=True) unions all fields and fills gaps with NaN — which is a good reason to prefer it when the file fits in memory.
Skipping bad lines
Because each line is independent, one corrupt line does not have to stop the job:
import json
good, bad = [], 0
with open("events.jsonl", encoding="utf-8") as f:
for n, line in enumerate(f, start=1):
line = line.strip()
if not line:
continue
try:
good.append(json.loads(line))
except json.JSONDecodeError as e:
bad += 1
print(f"line {n}: {e.msg}")
print(f"parsed {len(good)}, skipped {bad}")This resilience is one of the format's genuine advantages. With a single large JSON array, one bad character anywhere invalidates the entire document.
Going the other way: Excel to JSONL
To produce JSONL from a spreadsheet — a common need when preparing training data:
import pandas as pd
df = pd.read_excel("data.xlsx")
df.to_json("data.jsonl", orient="records", lines=True, force_ascii=False)force_ascii=False keeps non-English characters readable instead of escaping them to é sequences. See also converting Excel to JSON.
Quick reference
| Symptom | Meaning |
|---|---|
ValueError: Trailing data | JSONL read as JSON — add lines=True |
Extra data: line 2 column 1 | Same cause, from json.load() |
Expecting value: line 1 column 1 | Blank line in the file — skip empty lines |
Unexpected token { in JSON | JavaScript parser hitting the second record |
| Only some columns appear | Ragged records — union the keys first |
Converted your JSONL to a JSON array?
Drop it into the converter and get a clean Excel file — nested fields flattened automatically, all processed in your browser.
Convert JSON to Excel Now →