
I kept getting JSON from APIs that I needed to look at as a table or send to someone who only uses Excel.
The obvious fix is one of the many online JSON-to-Excel converters. But most of them send what you paste to their server, and I didn't love doing that with real data. So I made my own: jsoncsvvisualizer.com.
It does two things. You can paste JSON or CSV and see it as a table, then download it as .xlsx. And there's a JSON formatter that also fixes broken JSON and tells you where it was broken.
It's React + Vite + Tailwind, SheetJS for the Excel file, and it's just static files on Render. There's no backend.
A few parts were more interesting than I expected, so I'm writing them down.
A Content Security Policy as an extra layer
Every converter site says it doesn't keep your data. I wanted something stronger than a sentence in a privacy policy.
The site sends this Content Security Policy header:
default-src 'self';
script-src 'self';
connect-src 'self';
connect-src 'self' tells the browser to block fetch/XHR requests to other domains. It's not a full guarantee (it still allows requests to the site's own domain, and doesn't cover every channel), but there's also no backend to receive anything, and you can check the Network tab yourself while using it.
One thing that tripped me up: Vite's dev server needs inline scripts and websockets for hot reload, so the strict policy breaks npm run dev. I ended up with a relaxed policy for dev and the real one for vite preview, so if I break the CSP I find out locally instead of in production.
Finding where JSON is broken
People paste stuff like this:
{
name: 'EleStack',
tools: ['Grid', 'JSON',],
active: True
}
The jsonrepair library fixes this fine. But I didn't want to quietly change someone's data without telling them, so the tool shows a warning like "input had errors, line 3 col 26", and clicking it jumps the cursor there.
Getting that line number was harder than I thought. My first attempt read the position out of the JSON.parse error message. Turns out Chrome, Firefox and Safari all word that message differently, and Safari often doesn't include a position at all.
So I gave up on parsing error messages and wrote a small strict JSON scanner, about 60 lines, that walks the text and stops at the first character that breaks the rules. Then it's just:
const before = text.slice(0, position);
const line = before.split('\n').length;
const column = position - before.lastIndexOf('\n');
Same result in every browser.
Data pasted twice
This one I only found by testing: if you accidentally paste the same array twice, you get [...] [...], which isn't valid JSON, and the repair step makes a mess of it.
The fix was to split the input into separate top-level values first, by counting brackets (and ignoring brackets inside strings), then repair each piece and merge the arrays.
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (inString) { /* skip until the closing quote */ continue; }
if (ch === '"') {
inString = true;
} else if (ch === '{' || ch === '[') {
if (depth === 0) start = i;
depth++;
} else if (ch === '}' || ch === ']') {
depth--;
if (depth === 0) chunks.push(text.slice(start, i + 1));
}
}
Records that don't match
API data is rarely uniform. One record has email, the next doesn't. If you take the columns from the first row, you lose fields. So the columns are every key that appears in any row, and missing values are just empty cells:
const keys = new Set();
rows.forEach(row => Object.keys(row).forEach(k => keys.add(k)));
Nested objects get shown as JSON text so nothing disappears in the Excel file.
Things I got wrong
I launched with the same page title on every page, which is pretty bad for search. And the navbar was too wide on phones, so every page scrolled sideways. I only noticed after it was live. Both are fixed now.
If you want to try it
jsoncsvvisualizer.com. It's free, with no account and no cookies.
If you have a really broken JSON file, I'd like to know if it survives. I'm also deciding what to add next, probably CSV to JSON or a tree viewer. Let me know which you'd actually use.
Top comments (2)
The union-of-keys export is a useful choice. I'd add a round-trip fixture with an ID like "00123", a quoted 20-digit ID, null versus a missing key, and a string starting with "=SUM(1,2)". Reopen the .xlsx and check cell types as well as displayed values, so Excel doesn't quietly change the meaning.
One wording detail: connect-src 'self' still allows requests to the converter's own origin. It blocks cross-origin fetches, not all possible uploads. "No backend, processes locally" and a network check with synthetic input are narrower claims than "can't upload even if it wanted to."
Thank you for your feedback, I appreciate it