Three layouts, one object
The layout buttons above change the elements the converter reaches for, nothing else. Same input, same escaping, different container. Here is a two key object under each one.
{"code": "NW-4", "onTimeRate": 0.91}Table
A key column and a value column, with <th scope="row"> on the key so screen readers announce it as the row heading.
<table class="jx-table jx-pairs"><tbody><tr><th scope="row">code</th><td>NW-4</td></tr></tbody></table>Nested list
Objects become <ul>, arrays become <ol> so the index stays visible. The lightest option on a phone.
<ul class="jx-list"><li><span class="jx-key">code</span> NW-4</li></ul>Definition list
A <dt> per key with its <dd> underneath. The right pick when the output is documentation rather than data.
<dl class="jx-dl"><dt>code</dt><dd>NW-4</dd></dl>One rule sits above the layout choice. An array whose items are all objects becomes a real <table> with a <thead> in table mode, because a row set is the one JSON shape a table was built for. Arrays of numbers or strings never become tables. They become a list, since a single column table is a list wearing borders.
A table needs rows that agree
Header columns come from the union of every key in the array, in first seen order. Load the sample and look at the carriers table. Two carriers have five keys. Baltic Rail carries a sixth key, note, so the header grows to six columns and the other two rows get an empty cell marked class="jx-missing".
Empty cells are honest output, not a bug, though they are worth reading as a signal. A response where half the rows are missing half the columns is usually two record types sharing one array. Splitting the array upstream produces two clean tables instead of one ragged one. When the shape is genuinely optional data, style .jx-missing with a dash or a muted background so a reader tells a blank apart from a zero.
Key order comes from the JSON itself. Objects keep insertion order, so the column order matches the order your API wrote the fields. Sorting the keys before pasting is the way to force a different column order.
Escaping happens before anything reaches the page
JSON strings hold whatever the source system stored, and that includes markup. A product name of <script>alert(1)</script> converts to escaped text, never to a live tag. Five characters are replaced every time a key or a value is written out: &, <, >, ", and '. Keys go through the same path, since a hostile key name is as dangerous as a hostile value.
Two string shapes get promoted to links. A value matching https:// or http:// end to end becomes an anchor, and a plain address becomes a mailto: link. The match has to cover the whole string. A sentence with a URL inside it stays text, which keeps the converter from guessing at prose. The Rendered tab writes the output into a sandboxed iframe with scripting switched off, so previewing untrusted data stays contained.
Escaping protects the page you paste into. It says nothing about whether the data belongs there. Salaries, tokens and internal IDs are still readable to whoever opens the HTML.
Class names, styles and the wrapper
The class prefix field renames every generated hook at once. Set it to report and you get report-table, report-list, report-key. Clearing the field drops the prefix entirely and leaves bare names like table and key, which suits a page where those classes already exist in your stylesheet.
Include style block adds a small <style> element above the markup with borders, header shading and colored numbers. It is a legible default, not a design. Turn it off once the fragment lives inside a site with its own CSS, otherwise the block competes with your own rules on every paste.
Full HTML page wraps the result in a doctype, a head with the charset and viewport meta tags, and a <main> element. Use it when the download is meant to open on its own. Leave it off when the output is going into a Blade template, a React component or a CMS field.
Where this stops short
- Nothing is paginated. A 10,000 row array produces a 10,000 row table, which is a slow paint in the Rendered tab and a slower one in a browser you ship it to.
- Deep nesting in table mode puts tables inside table cells. Past three levels this gets hard to read on a narrow screen. Nested list mode handles depth much better.
- Columns are neither sorted nor filtered nor reordered. The output is static markup with no JavaScript attached.
- Arrays of arrays become nested ordered lists rather than a grid, because JSON gives no header row to build one from.
- Dates, currency and units stay exactly as written.
0.91renders as0.91, not as 91 percent. - Styling is class based, so the output is not ready for HTML email. Mail clients need inline styles, which means running the fragment through an inliner first.
- Malformed input is repaired before parsing, so trailing commas and single quotes get through. A repair changes what you meant sometimes, so check the output against the source when the input was broken.
The same conversion in your own code
Pasting works for a one off. A recurring report belongs in the codebase. In PHP, a row set is a foreach over the union of keys.
$rows = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);$cols = array_keys(array_merge(...array_map('array_keys', $rows)));echo '<table><thead><tr>';foreach ($cols as $col) {echo '<th scope="col">' . htmlspecialchars($col, ENT_QUOTES) . '</th>';}
echo '</tr></thead><tbody>';foreach ($rows as $row) {echo '<tr>';foreach ($cols as $col) {echo '<td>' . htmlspecialchars((string) ($row[$col] ?? ''), ENT_QUOTES) . '</td>';}
echo '</tr>';}
echo '</tbody></table>';In a Blade view the escaping is already handled, so the loop shrinks to the structure.
<table><thead><tr>@foreach($cols as $col)<th scope="col">{{ $col }}</th>@endforeach</tr></thead><tbody>@foreach($rows as $row)<tr>@foreach($cols as $col)<td>{{ $row[$col] ?? '' }}</td>@endforeach</tr>@endforeach
</tbody></table>The rule worth carrying over from this page is the escape call on every key and every value, including the ones you believe are safe. Data that looks clean in staging arrives with an ampersand in it eventually.
