file2markdown
jsonljsonmarkdownconverterdeveloperllm

JSONL to Markdown: Convert JSON Lines Files to Clean Tables

November 7, 2026

JSONL to Markdown: Convert JSON Lines Files to Clean Tables

If you have ever opened a .jsonl file in a text editor, you know the problem: it is hundreds or thousands of lines, each one a dense, single-line JSON object. There are no line breaks inside a record, no table, nothing a human can scan. JSONL (also called JSON Lines or NDJSON) is built for machines to stream one record at a time, not for people to read.

That is exactly the problem file2markdown exists to solve for file formats in general: turn something a machine produced into something a person, or an LLM, can actually read. JSONL needs one extra step that plain JSON does not, because of how the format is structured (file2markdown does not accept .jsonl uploads), so this guide covers that step and then hands off to the same free converters used for JSON to Markdown and CSV to Markdown.

Why JSONL Shows Up So Often in AI Work

JSONL is the default export format for a lot of the data AI builders touch daily:

  • Fine-tuning datasets. OpenAI's fine-tuning API, for example, expects the training file in JSONL, with one complete example (a messages array) on every line.
  • Chat and conversation logs. Conversation logs from agent frameworks and LLM tooling are often stored as JSONL, one turn or session per line.
  • Evaluation and eval-harness results. Tools that score LLM outputs (accuracy, latency, pass/fail) usually write one result row per line.
  • Log aggregation. Structured application logs are often written as JSONL because it is easy to append to and easy to parse line by line; jsonlines.org calls it "a great format for log files."

None of those are meant to be read directly. But reviewing a sample of training data before a fine-tuning run, sanity-checking an eval run, or pasting a few conversation turns into an LLM prompt all require turning that JSONL into something readable first.

Why JSONL Needs a Different Approach Than JSON

A .json file is normally one value: a single object, or an array of objects wrapped in [ and ]. A .jsonl file is different: each line is its own, independent JSON object, with no enclosing brackets and no commas between lines. Feed a .jsonl file to a standard json.load() call and it fails immediately, because the file as a whole is not valid JSON. Your tooling has to read it line by line instead.

That distinction is the reason this needs its own walkthrough rather than reusing the plain-JSON steps: the parsing step is different, even though the end goal, a clean Markdown table, is the same.

Option 1: Flatten JSONL to a Markdown Table with Python

If your JSONL records are flat-ish objects (a fine-tuning example, a log line, an eval result), pandas reads line-delimited JSON natively:

import pandas as pd

df = pd.read_json("data.jsonl", lines=True)
print(df.to_markdown(index=False))

The lines=True flag is the whole trick: it tells pandas to treat the file as one record per line instead of one big JSON document. This needs pip install pandas tabulate. Nested objects get flattened into dotted column names (user.name) if you run pd.json_normalize(df.to_dict("records")) before the to_markdown() call. Arrays, such as the messages list in a chat export, are not expanded: each one stays in a single cell.

Option 2: Wrap the Lines in a JSON Array, Then Use the JSON Converter

file2markdown rejects .jsonl uploads, and a JSON Lines file renamed to .json is rejected too, with a message that it looks like JSON Lines. Wrap the lines in one JSON array ([ before the first line, ] after the last, commas between records) and the file becomes valid JSON that the JSON to Markdown converter can turn into a real Markdown table.

With jq, that is one command (-s is --slurp, which reads every line into one array):

jq -s . data.jsonl > data.json

Without jq, this Python snippet adds the brackets and commas and leaves the text of each record as it was:

with open("data.jsonl", encoding="utf-8") as src:
    records = [line.strip() for line in src if line.strip()]
with open("data.json", "w", encoding="utf-8") as out:
    out.write("[\n" + ",\n".join(records) + "\n]\n")

Then upload data.json to the JSON to Markdown converter:

  • A list of objects becomes one Markdown table when the objects have at most 30 distinct keys between them: one column per key in first-seen order, an empty cell where a record lacks a key, and null, true and false in their JSON spelling. With more keys you get a numbered list instead.
  • Numbers stay exactly as written, so 1.10 stays 1.10.
  • Nested values are not flattened into dotted columns (user.name). A nested object or array inside a record shows up in its cell as compact inline JSON, cut at 200 characters.

Copy the result or download the .md file.

Option 3: Convert JSONL to CSV, Then Use the CSV Converter

If you want a CSV copy of the data anyway, for a spreadsheet for example, the CSV route ends at the same kind of Markdown table:

  1. Convert the JSONL to CSV. In Python: pd.read_json("data.jsonl", lines=True).to_csv("data.csv", index=False). To spread nested objects over dotted columns on the way, which the JSON converter does not do, use pd.json_normalize(pd.read_json("data.jsonl", lines=True).to_dict("records")).to_csv("data.csv", index=False). pandas parses the values first, so 1.10 comes out as 1.1.
  2. Upload the CSV to the CSV to Markdown converter. file2markdown's CSV converter turns rows and columns into a Markdown table, and detects comma, semicolon, tab and pipe delimiters.
  3. Download or copy the Markdown. You now have a table ready to paste into documentation, an Obsidian vault, or an LLM prompt.

A Real Workflow: Reviewing Fine-Tuning Data Before a Training Run

A common case: you have a train.jsonl file with 500 examples, each one a {"messages": [...]} object, and you want to spot-check 20 of them before kicking off a fine-tuning job.

  1. Sample the file: head -20 train.jsonl > sample.jsonl (or the equivalent in Python).
  2. Give every message its own row and wrap the result in a JSON array: jq -s '[keys[] as $i | .[$i].messages[] | {example: ($i + 1), role, content}]' sample.jsonl > sample.json.
  3. Upload sample.json to the JSON to Markdown converter and get one table with example, role and content columns that you can actually scan for bad examples, empty turns, or formatting mistakes, before you spend compute on a bad dataset.

Step 2 matters for chat data. Upload the examples as they are and each row holds its whole messages array as inline JSON, cut at 200 characters.

The same pattern works for a chat-export JSONL you want to feed into an LLM for summarization: in a Markdown table each key is written once, as a column header, instead of once per record, and the brackets and quotes are gone. See why Markdown is the format LLMs prefer for the token-efficiency case in more detail.

Frequently Asked Questions

Can I upload a .jsonl file directly to file2markdown?

No. file2markdown rejects .jsonl uploads with a message, and a JSON Lines file renamed to .json is rejected as well. Wrap the lines into a valid JSON array first (Option 2 above) or convert them to CSV (Option 3), then upload that file.

What is the difference between JSONL and NDJSON?

They are effectively the same format: one JSON value per line (usually an object), no surrounding array. "JSONL" (JSON Lines) and "NDJSON" (Newline Delimited JSON) are two names for the same convention; jsonlines.org describes JSON Lines as "also called newline-delimited JSON."

Why does my JSONL file fail when I try to parse it as JSON?

Because a .jsonl file is not one JSON document, it is many independent ones separated by newlines. A standard json.load() or JSON.parse() call expects a single value and throws an error where the second line starts (Python reports Extra data: line 2 column 1). Use a line-by-line reader, or a library flag like pandas' lines=True, instead.

Will this work for deeply nested JSONL records?

Mostly, with one extra step. pd.json_normalize() flattens nested objects into dotted column names (user.name, metadata.source), but arrays inside a field still end up in a single cell. file2markdown's JSON converter does not flatten at all: a nested object or array in a table cell is shown as inline JSON, cut at 200 characters. For records more than two or three levels deep, pull out the fields you actually need first, in Python or with jq as in the fine-tuning example above, then convert.

Got a JSONL file to clean up? Wrap it in a JSON array and convert it to Markdown now.

The Markdown Memo

A fortnightly note for lawyers, researchers, accountants, and anyone else drowning in PDFs, scans, and decks. No spam.