JSON to HTML Table Converter

Show an API response to somebody who does not read JSON. A real table with a real header, nested fields flattened into dotted columns, everything escaped.

To convert JSON to an HTML table, paste your array of objects above. The keys become header cells in a thead, each object becomes a row in a tbody, nested objects flatten into dotted columns, and every value is escaped so nothing in your data can break the page. Take a bare fragment or a complete styled document.

Want to pick the columns first? Open the app

The audience for a table is not the audience for JSON

The reason this conversion keeps being needed is that JSON is for programs and tables are for people. An engineer has the response; a colleague in support, finance or operations needs to look at the rows and will not open a text editor to do it.

The second case is documentation. An API reference that shows a sample response as a table alongside the JSON is dramatically easier to scan, and building that table by hand goes stale the first time the response changes.

Worked example

An order response with a nested customer object:

[
  {
    "order_id": "ORD-00001",
    "placed_at": "2024-05-13T01:17:00Z",
    "status": "shipped",
    "customer": { "id": 4188, "name": "Katherine Johnson", "city": "Osaka" },
    "items": 8,
    "total": 2151.83,
    "gift": false
  },
  {
    "order_id": "ORD-00002",
    "placed_at": "2024-08-10T11:39:00Z",
    "status": "delivered",
    "customer": { "id": 3737, "name": "Ada Lovelace", "city": "Berlin" },
    "items": 1,
    "total": 1783.46,
    "gift": true
  }
]

And the fragment:

<table>
  <thead>
    <tr><th>order_id</th><th>placed_at</th><th>status</th><th class="num">customer.id</th><th>customer.name</th><th>customer.city</th><th class="num">items</th><th class="num">total</th><th>gift</th></tr>
  </thead>
  <tbody>
    <tr><td>ORD-00001</td><td>2024-05-13T01:17:00Z</td><td>shipped</td><td class="num">4188</td><td>Katherine Johnson</td><td>Osaka</td><td class="num">8</td><td class="num">2151.83</td><td>false</td></tr>
  </tbody>
</table>

The customer object became three columns. Only the genuinely numeric ones carry the num class: customer.id, items and total. The timestamp is left as text, because it is a string in the source and interpreting it would mean choosing a timezone nobody asked for.

What flattening does to a nested object

A nested object becomes dotted columns. "customer": { "name": "Ada" } becomes a column called customer.name. That is the one convention every tool in this space agrees on, and it is reversible enough that a person reading the header knows exactly where the value came from.

Nesting deeper than four levels is kept as JSON text in a single cell instead of exploding into columns nobody will use. Four is deep enough for every API response worth tabulating and shallow enough that the header row stays readable.

An array of objects is also kept as JSON text in one cell, rather than being spread across extra rows. Spreading it would silently change the row count, so a file of 200 orders would come back as 4,000 rows and the totals would all be wrong. If that is the shape you want, it is a join rather than a flatten, and the full editor does it explicitly.

Every one of those decisions is reported under the result, with a count. Nothing about the structure changes quietly.

What counts as JSON here

  • An array of objects is the ordinary case, one row per object.
  • A single object becomes a one-row table, which is what you want when the response is a single record.
  • An object wrapping an array such as { "data": [ ... ] } is unwrapped automatically, because that is what most APIs return and unwrapping it by hand is tedious.
  • JSON Lines is recognised when the whole document fails to parse but every line parses on its own, with a note saying that is what happened.
  • A file with one bad line out of many gets an error naming the line rather than a character offset, because "unexpected token at position 4213" tells you nothing useful about a 40,000-line file.

Escaping, and why the apostrophe matters

All five markup characters are escaped, apostrophe included. JSON string values routinely hold text written for people, which means ampersands in company names, angle brackets in comparison text and apostrophes in surnames.

The apostrophe is the one most converters skip, because a bare one renders perfectly inside a cell. It stops being fine the moment the markup is reused, and reuse is the normal case: pasted into a single-quoted attribute, dropped into a JavaScript string, run through a template. &#39; costs nothing and removes the whole class of problem.

A newline inside a JSON string becomes a <br>, so a multi-line note renders as multiple lines instead of collapsing under HTML's whitespace rules.

Frequently Asked Questions

What happens to nested objects?

They flatten into dotted columns, so a customer object with a name in it becomes a customer.name column. Nesting deeper than four levels is kept as JSON text in one cell rather than producing a header row nobody can read.

What about an array of objects inside a record?

It is kept as JSON text in a single cell rather than being spread across extra rows. Spreading it would change the row count silently, so a table of 200 orders would come back as 4,000 rows and every total on the page would be wrong.

Does it handle an API response wrapped in a data key?

Yes, automatically. An object whose single array property holds the records is unwrapped, because that is the shape most APIs return and unwrapping it by hand before every conversion is pointless work.

Are values escaped?

All five markup characters, apostrophe included. JSON holds text written for people, so ampersands and apostrophes are common, and the apostrophe is the one that breaks when the markup gets pasted inside a single-quoted attribute.

Why is my timestamp column not formatted?

Because it is a string in the source and reformatting it means choosing a timezone and a locale, neither of which a converter can know. It comes through exactly as written, which is the only lossless answer.

Can I get a complete page rather than a fragment?

Yes. The full page option wraps the table in a document with a doctype, a character set, a title and a compact stylesheet, so you can save the file and open it without touching anything.

Show the response as a table

Semantic markup, nested fields flattened, everything escaped. Copy and paste.

Back to the converter