JSON to Markdown

Turn JSON into a Markdown table or a nested list. Free, no account. Paste an API payload, a model’s structured output, or JSON Lines, or upload .json, .jsonl, .ndjson, or .txt. Parsing stays in your browser. The payload is not uploaded.

Want a grid you can edit cell by cell? Open the Markdown Table Generator. This page does not transfer the result. A comma or semicolon file belongs on CSV to Markdown.

FileReader reads the .json or .jsonl on this device. The payload is not posted anywhere. A paste over 5,000,000 characters, or a file over that many bytes, is refused.

An array of objects becomes columns. An array of values becomes one column. An array of arrays uses the first array as the header. One object becomes Key and Value.

Waiting for JSON. Parsing stays in this browser.

Paste JSON or JSON Lines, or upload a file. Copy and Download stay off until there is Markdown.

Markdown Table Generator · CSV to Markdown · All tools

How to convert JSON to Markdown

Three steps on this page. Sample JSON loads the array in the worked example, with flatten on, the list switch off, and pretty-print on. Invalid JSON stays in the box and shows a line and column. Nothing is posted.

1

Paste JSON or upload a file

Paste a JSON value or JSON Lines, or choose a .json, .jsonl, .ndjson, or .txt file. FileReader reads the file locally. Copy and Download stay disabled until the paste parses.

2

Choose a table or a nested list

Leave Flatten nested keys on to expand objects into a.b.c columns, or turn it off to inline nested values as JSON in backticks. Output as list instead of table writes nested bullets for objects. Pretty-print pads table columns only.

3

Copy, download, or preview

The Markdown updates as you edit. Copy it, download a .md file, or open Preview. Invalid JSON names a line and column. A paste over 5,000,000 characters is refused.

Which JSON shape becomes which table

“json to markdown” and “json to markdown table” usually mean one of four shapes, and this page treats them differently on purpose. An array of objects, the usual API list, becomes a table whose columns are keys. An array of plain values (strings, numbers, booleans, or null) becomes a single column named Value. An array of arrays becomes rows, and the first inner array is the header, so a matrix that already includes labels keeps those labels. A nested object inside one of those cells stays in the cell: dotted pairs such as city: Taipei when flatten is on, or JSON in backticks when it is off. It does not grow new columns. One object becomes two columns, Key and Value. A top-level string, number, boolean, or null becomes a Value column with one data row. An empty array or an empty object does not invent a row. The status line says there is nothing to copy.

Column order for objects is the order keys are first seen, not alphabetical order. The converter walks the first object from top to bottom, then appends any new key from the next object. A later object that omits a key gets a blank cell. A later object that adds a key adds a column at the end. That is the stable order you can describe to someone else: it matches the file, not a sort. If the array mixes objects with plain values or with inner arrays, the page does not guess a schema. Each item becomes one row under Value, and the status line says the array was mixed.

Numbers, booleans, and null are formatted after JSON.parse. null is the word null, so it is not the same as a missing key. true and false stay words. A number is JavaScript’s default string for that number: a trailing “.0” disappears, and scientific notation such as 1e2 becomes 100. Integers larger than 9007199254740991 cannot be exact in ordinary JSON.parse, and this page does not swap in another parser, so those ids should be quoted strings in the source if you need every digit. Strings are copied as text. A newline, a carriage return, or a Unicode line separator in a string is written as a break tag inside the cell, because a pipe table is one line per row. A pipe character is escaped.

Nested data, lists, and JSON Lines

API objects are rarely flat. Flatten nested keys, on by default, walks objects and arrays and builds names like profile.city and tags.0. The index is a decimal segment, not brackets, so it stays a single Markdown header cell. An empty nested object still keeps its key, with a blank value, so you can see that the field was present. Turn flatten off when you would rather read the nested JSON than spread it across columns. The cell then holds JSON.stringify of that value, wrapped in backticks. If the JSON itself contains a backtick, the fence grows until it no longer collides. Pipes inside that JSON are escaped the same way as any other cell, so the table does not gain a surprise column. The small object below is flatten off and pretty-print off, which is not the default you get from Sample JSON.

| Key | Value |
| --- | --- |
| profile | `{"city":"Taipei","note":"a\|b"}` |

Output as list instead of table is for notes and for prompts that read better as bullets than as a wide pipe row. A single object lists each key in bold, and nests child objects and arrays underneath. An array of objects numbers each item, then nests that item’s keys. Flatten does not apply in list mode, and pretty-print does not either. The switches are disabled while the list is on so the screen does not pretend they change the bullets. An empty string in a list is written as (empty). In a table, an empty string stays an empty cell, which is different from null. The list below is the converter with the list switch on. Sample JSON does not flip that switch for you.

Object

{
  "name": "Mei",
  "profile": { "city": "Taipei", "tags": ["editor"] },
  "active": false
}

Nested list

- **name:** Mei
- **profile:**
  - **city:** Taipei
  - **tags:**
    - editor
- **active:** false

Models that emit structured output often return one JSON object, or a fenced block you can paste after you remove the fence. Paste the JSON only. A leading ```json line is not JSON, and the error will point at it. JSON Lines is the other shape this page accepts: a log or an export where each line is one value. The whole text is parsed first. Only when that fails, and every non-empty line parses on its own, are the lines treated as records. Blank lines are skipped. The status line reports the record count. A pretty-printed array is one value, not JSON Lines, because the bracket lines are not JSON by themselves. Comments and trailing commas are syntax errors. The two-line log below is JSON Lines, pretty-print off.

{"id":"usr_1","name":"Mei","active":true}
{"id":"usr_2","name":"Omar","active":false,"note":null}

Read 2 JSON Lines records because the paste is not a single JSON value.

| id | name | active | note |
| --- | --- | --- | --- |
| usr_1 | Mei | true |  |
| usr_2 | Omar | false | null |

A worked example from an API array

Sample JSON is this array. The first user has a nested profile, two tags, a boolean, a fractional score, and a note that contains a pipe. The second user has a null score, no tags, and an extra retired flag. With flatten on, list off, and pretty-print on, the columns run id, name, active, score, profile.city, profile.tags.0, profile.tags.1, note, then retired. Mei’s retired cell is blank because that key is absent. Omar’s tag cells are blank because those keys are absent. His score is the word null because the key is present and the value is null. The pipe in the note is escaped. Pretty-print pads the raw columns; Preview does not need that padding.

JSON

[
  {
    "id": "usr_1",
    "name": "Mei Lin",
    "active": true,
    "score": 12.5,
    "profile": { "city": "Taipei", "tags": ["editor", "zh"] },
    "note": "Uses | pipes"
  },
  {
    "id": "usr_2",
    "name": "Omar",
    "active": false,
    "score": null,
    "profile": { "city": "Amman" },
    "retired": false
  }
]

Markdown

| id    | name    | active | score | profile.city | profile.tags.0 | profile.tags.1 | note          | retired |
| ----- | ------- | ------ | ----- | ------------ | -------------- | -------------- | ------------- | ------- |
| usr_1 | Mei Lin | true   | 12.5  | Taipei       | editor         | zh             | Uses \| pipes |         |
| usr_2 | Omar    | false  | null  | Amman        |                |                |               | false   |

A broken payload should say where it broke. This fragment is missing a value after the colon. The converter does not invent one. The message under the fragment is the same string the tool shows for that paste, including the line and column.

{
  "sku": }

Invalid JSON at line 2, column 10: Unexpected token '}', "{ "sku": }" is not valid JSON

Limits, hand conversion, and the extension

Copying keys into a Markdown table by hand is fine for a three-field object. It slips when the second record has a key the first one lacked, when a string contains a pipe or a newline, or when a nested array should become tags.0 and tags.1 rather than the text [object Object]. This page does that bookkeeping in the browser. It refuses a paste longer than 5,000,000 characters, and the file input refuses a file larger than that many bytes, so a multi-megabyte log does not have to be parsed to show you an error. Under the cap, tables still stop at 1,000 rows and 100 columns. Lists stop at 1,000 top-level items. The status line names the cut. A wide flatten, dozens of dotted keys, hits the column cap the same way. Those status counts are the rows and columns actually written, after the cap. A payload that is valid JSON but nested thousands of levels deep does not become a table either. The converter stops and says the value is too deep for the browser call stack, instead of letting the page throw.

The result is text you can put in a GitHub issue, an Obsidian note, or a prompt. It is not a schema validator and it does not call the remote API for you. Paste the body you already have. When the job is a docs diff or one public fetch instead of a payload you already hold, that split is the developer use case. If that body is HTML from a response you copied out of devtools, use HTML to Markdown instead. If you need the pipe table as HTML, continue in Markdown to HTML. If the prompt budget matters, paste the Markdown into the Token Counter. That counter is explicit about OpenAI encodings. A delimited spreadsheet export is CSV to Markdown. Per-column alignment and a grid you type into live on the Markdown Table Generator. The tools hub is the index when you are not sure which input you have.

If the body is an HTML error page and you want the words without tags, HTML to Text is the page for that. This page stays on JSON values.

The Chrome extension converts the document in a tab you already opened, including pages that require your session. It writes Markdown from that page. It does not parse a JSON file you downloaded, and this tool does not see cookies or a logged-in response. If the JSON only exists behind a login, fetch or copy it yourself, then paste it here. Do not expect the URL tool to turn an authenticated JSON endpoint into this table. After the table exists, Preview on this page uses the same Markdown renderer as the other tools, so a broken pipe is visible before you commit the file.

JobThis pageBy handCSV to Markdown
API array of objectsUnion of keysRetype each fieldOnly if you flatten first
Nested objecta.b.c columns, or JSON in a cellEasy to drop a keyNo nesting
JSON Lines logOne record per lineJoin the lines yourselfWrong parser
Runs in the browserYesYesYes
Add the WebInk extension

The extension clips web pages. It does not open a JSON file.

Frequently asked questions

No. JSON.parse runs in the page, and the Markdown is built beside it. Uploading a .json or .jsonl file uses FileReader, which reads the bytes locally. This page does not call /api/markdown. There is no account. A file over 5 MB is not read. A paste over 5,000,000 characters returns an error and no table. Copy and Download stay on your machine.

Related