How to Convert JSON Lines (JSONL / NDJSON) to Excel

If your file starts with { on every line instead of [ at the top, it is not broken JSON — it is a different format, and standard JSON parsers will reject it.

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:

  1. Replace \n (newline) with ,\n to put commas between records.
  2. Remove the trailing comma on the last line.
  3. 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

SymptomMeaning
ValueError: Trailing dataJSONL read as JSON — add lines=True
Extra data: line 2 column 1Same cause, from json.load()
Expecting value: line 1 column 1Blank line in the file — skip empty lines
Unexpected token { in JSONJavaScript parser hitting the second record
Only some columns appearRagged 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 →