jsonfmt: A Zero-Dependency JSON Formatter That Survived 40MB Paste Tests
Built a browser JSON formatter after every online tool choked on a minified API dump. Full vanilla JS, syntax validation, and the parse errors that actually matter.
Tested on: Chrome 136, Firefox 139, Safari 18
Problem: Pasted a 38MB minified package-lock.json into three online formatters. Two tabs froze. One showed Unexpected token with no line number.
Needed something I could host on a static page with zero npm install for visitors.
What it does
Paste ugly JSON → click Format → pretty-print with 2-space indent
Invalid JSON → red error box with line/column from native SyntaxError
Full single-file tool (copy-paste)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JSON Formatter</title>
<style>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
textarea { width: 100%; min-height: 220px; font-family: ui-monospace, monospace; font-size: 13px; }
.row { display: flex; gap: .5rem; margin: .75rem 0; flex-wrap: wrap; }
button { padding: .5rem 1rem; cursor: pointer; }
#error { color: #b91c1c; background: #fef2f2; padding: .75rem; border-radius: 6px; display: none; }
#error.show { display: block; }
label { font-size: .875rem; }
</style>
</head>
<body>
<h1>JSON Formatter</h1>
<textarea id="input" placeholder='{"key":"value"}'></textarea>
<div class="row">
<button type="button" id="format">Format</button>
<button type="button" id="minify">Minify</button>
<button type="button" id="clear">Clear</button>
<label><input type="number" id="indent" value="2" min="0" max="8" style="width:3rem"> spaces</label>
</div>
<div id="error" role="alert"></div>
<textarea id="output" readonly placeholder="Output"></textarea>
<script>
const input = document.getElementById('input');
const output = document.getElementById('output');
const errorEl = document.getElementById('error');
const indentEl = document.getElementById('indent');
function showError(msg) {
errorEl.textContent = msg;
errorEl.classList.add('show');
output.value = '';
}
function clearError() {
errorEl.textContent = '';
errorEl.classList.remove('show');
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch (e) {
const match = e.message.match(/position (\d+)/i);
if (match) {
const pos = Number(match[1]);
const before = text.slice(0, pos);
const line = before.split('\n').length;
const col = before.length - before.lastIndexOf('\n');
throw new Error(e.message + ' (line ' + line + ', col ' + col + ')');
}
throw e;
}
}
document.getElementById('format').addEventListener('click', () => {
clearError();
const raw = input.value.trim();
if (!raw) return;
try {
const obj = parseJson(raw);
const spaces = Number(indentEl.value) || 0;
output.value = JSON.stringify(obj, null, spaces);
} catch (e) {
showError(e.message);
}
});
document.getElementById('minify').addEventListener('click', () => {
clearError();
const raw = input.value.trim();
if (!raw) return;
try {
output.value = JSON.stringify(parseJson(raw));
} catch (e) {
showError(e.message);
}
});
document.getElementById('clear').addEventListener('click', () => {
input.value = '';
output.value = '';
clearError();
});
</script>
</body>
</html>
Error I kept hitting
JSON.parse: Unexpected token u in JSON at position 0
Cause: pasted undefined from a console.log. JSON has no undefined — only null.
Trailing comma trap
{"a": 1,}
Strict JSON.parse rejects trailing commas. My error handler now surfaces line/col so you find the comma in seconds.
Performance note
For files over ~50MB, add a warning:
if (raw.length > 50_000_000) {
if (!confirm('Large input may freeze the tab. Continue?')) return;
}
38MB formatted in ~800ms on M2 MacBook. Acceptable for a static tool page.
Verify
- Save as
jsonfmt.html, open vianpx serve . - Paste invalid
{"a":}— error shows line 1 - Paste valid minified JSON — output is indented
- Lighthouse: 100 Performance (no external scripts)
Deploy
Drop the file in static/tools/jsonfmt/index.html on Hugo/VitePress/Astro. One HTML file, no build step.