JSON Formatter
Format, minify, validate, and explore JSON with a collapsible tree view. Paste any JSON, see it structured — all client-side, nothing leaves your browser. Three steps to build this tool from scratch.
📋 Try the JSON Formatter
Paste JSON below, then format, minify, validate, or explore it in the tree view. All 100% client-side, nothing leaves your browser.
Build the Parser
Why learn this
Every developer works with JSON daily — API responses, config files, data exports, logs. JSON is the universal data exchange format on the web. Understanding how to parse, format, and manipulate JSON is a fundamental skill — and building your own formatter gives you complete control over how data is displayed, validated, and exported.
What you built and why this way
JSON.parse + JSON.stringify, nothing else. The browser's native JSON API is surprisingly powerful. JSON.parse validates and parses in one call, throwing a descriptive error with line and column position when the input is malformed. JSON.stringify handles formatting with its built-in space parameter — pass a number (2, 4) for spaces or a string ("\t") for tabs. This approach needs zero dependencies, loads instantly, and works offline. The tradeoff: no custom error messages and no tolerance for trailing commas or single-quoted keys. If you need those, you'd reach for a library like json5 or parse-json. For 95% of use cases, native JSON is the right answer.
Format and minify as two operations. Formatting expands minified JSON into readable, indented output. Minifying collapses formatted JSON into a compact string — useful for storage, transmission, or pasting into character-limited fields. Both operations are just JSON.stringify with different spacing: null, indent for format vs null, 0 for minify. The two-button design makes the intent explicit.
Validate button as fast feedback. The validate button runs JSON.parse and reports success or the exact error message with line/column. This is separate from the main formatting pipeline because sometimes you just need to know "is this valid?" without reformatting. The error display uses the native SyntaxError.message which includes position info.
Indent selector instead of fixed indent. Different teams use different JSON style conventions. 2 spaces (JS standard), 4 spaces (Python convention), tabs (accessibility). The selector maps directly to JSON.stringify's space parameter.
Key concepts
JSON.parse(text)— Parses a JSON string into a JavaScript value. ThrowsSyntaxErrorwith position info if invalidJSON.stringify(value, null, space)— Converts a JS value to a JSON string. Thespaceparameter controls indentationSyntaxError.message— Built-in error messages include position info with line/column hintstry/catchfor JSON parsing — Always wrap JSON.parse in try/catch. Malformed JSON should show a clear error, not crash the page
Alternative approaches
- json5 parser — Allows trailing commas, single-quoted keys, comments. Useful for config files. Adds ~10KB
- Web Worker parsing — Offload JSON.parse to a background thread for very large payloads (>10MB)
- Streaming JSON parser (oboe.js, clarinet) — Parse JSON incrementally as it arrives. Unnecessary for a formatter that already has the full string
Browser compatibility
JSON.parseandJSON.stringify: supported in all browsers since IE8SyntaxError:messageformat varies slightly — Chrome includes "position X", Firefox includes "line X column Y"BlobandURL.createObjectURL: supported since IE10. Used for downloadnavigator.clipboard.writeText: Chrome 66+, Firefox 63+, Safari 13.1+. Falls back to execCommand
Performance notes
JSON.parseruns in O(n) time. A 1MB JSON string parses in ~10ms on modern hardwareJSON.stringifywith large objects can be slower than parsing — it walks the entire object tree- Zero memory allocations beyond the parsed object and output string. No framework overhead
Common pitfalls
- Trailing commas — JSON.parse rejects trailing commas in arrays and objects. #1 cause of "invalid JSON" from hand-written input
- Unescaped quotes inside strings — A string containing double quotes must escape them (
\") - Tab character in indentation — The indent value for tabs must be the actual tab character
'\t', not the string "tab" - Circular references — JSON.stringify throws "Converting circular structure to JSON" for self-referencing objects
Next up
Step 2 adds a collapsible tree view with color-coded values — making JSON explorable rather than just readable. Jump to Step 2 →
function formatJSON(text, indent) {
const parsed = JSON.parse(text);
const space = indent === 'tab'
? '\t' : parseInt(indent, 10);
return JSON.stringify(parsed, null, space);
}
function minifyJSON(text) {
return JSON.stringify(JSON.parse(text));
}
function validateJSON(text) {
try {
JSON.parse(text);
return { valid: true };
} catch (e) {
return { valid: false,
message: e.message };
}
} formatBtn.addEventListener('click', function() {
try {
const parsed = JSON.parse(input.value);
const indent = getIndent();
rawOutput.textContent =
JSON.stringify(parsed, null, indent);
buildTree(parsed);
clearError();
switchTab('tree');
} catch (e) {
showError(e);
}
});
minifyBtn.addEventListener('click', function() {
try {
const parsed = JSON.parse(input.value);
rawOutput.textContent = JSON.stringify(parsed);
buildTree(parsed);
clearError();
switchTab('raw');
} catch (e) { showError(e); }
}); JSON.parse throws a SyntaxError with position hints. Chrome says "position 42", Firefox says "line 2 column 10". Use a regex like /position (\d+)/ or /line (\d+) column (\d+)/i to extract location. The native error message is already good — just show it as-is.
Add Tree View
Why learn this
Raw JSON strings are hard to read — especially deeply nested objects with dozens of keys. A tree view lets you collapse irrelevant sections, expand what matters, and see the structure at a glance. Every modern API explorer (Postman, Insomnia, Chrome DevTools) uses a tree view because it's the fastest way to navigate complex JSON. Building one from scratch teaches recursive DOM rendering, event delegation, and state management without a framework.
What you built and why this way
Recursive DOM rendering (no libraries). The tree view is a single recursive function buildTree(value, depth) that returns a DOM element for each node. Primitives (strings, numbers, booleans, null) render as color-coded spans. Objects and arrays render as collapsible containers with a toggle arrow. The recursion mirrors the JSON structure itself — every object key becomes a subtree, every array index becomes a child node.
Color-coded value types. Strings are green (#4ade80), numbers are yellow (#facc15), booleans are purple (#c084fc), null is gray (#64748b), and keys are the accent blue (--accent). The colors match common syntax highlighting schemes — users recognize the type instantly without reading a legend.
Collapsible with click-to-toggle. Clicking an object key or array bracket toggles its children. The arrow rotates (▶ / ▼) to indicate state. Clicking a primitive value does nothing. The toggle uses e.stopPropagation() so clicking a nested item doesn't toggle its parent. State is stored in the DOM (display: none/block) rather than a separate data structure — simpler to implement and avoids sync bugs.
Two output tabs: Tree View and Raw. Some users want the visual tree (exploration), others want the formatted string (copy/paste). The tab switcher shows/hides panels with CSS classes. Both views stay in sync because they share the same input.
Key concepts
- Recursive DOM creation — Each
buildTreecall returns a DOM element. Objects/arrays call themselves for each child e.stopPropagation()— Prevents click events on child nodes from bubbling up to parent toggles- CSS-only collapsible state —
display: none/blocktoggled by a click handler textContentvsinnerHTML— Always usetextContentwhen setting user-provided values to prevent XSS
Alternative approaches
- <details> / <summary> elements — Native HTML collapsible elements. The browser handles open/close state. Styling is limited and the toggle arrow can't be easily customized
- Virtual scrolling for large JSON — For 100K-node JSON trees, render only visible nodes. Overkill for typical API responses (under 10K nodes)
- Canvas rendering — Pixel-perfect control over layout. Complex to implement and breaks accessibility
Browser compatibility
- DOM element creation (
document.createElement): supported in all browsers since IE6 element.addEventListener: supported since IE9- CSS
display: none/blocktoggle: supported in all browsers classListAPI: supported since IE10, Safari 5.1
Performance notes
- Tree rendering speed depends on node count. A typical API response (500 nodes) renders in under 2ms
- Each node is a lightweight
<div>with<span>children. Total overhead ~200 bytes per node - Collapsing/expanding triggers no re-renders — DOM elements are already created, just hidden
Common pitfalls
- stopPropagation on the wrong element — Call stopPropagation inside the toggle handler, not as a blanket on the container
- Displaying script tags in JSON values — Using
textContentprevents XSS. Never useinnerHTMLwith untrusted JSON values - Empty objects and arrays — Show "empty" label inside brackets instead of a blank collapsible
Next up
Step 3 adds copy, download, error highlighting, and live preview with debounced input. Jump to Step 3 →
function buildTree(value, depth) {
var t = typeof value;
if (value === null) {
return el('span', 'jf-tree-null', 'null');
}
if (t === 'string') {
return el('span', 'jf-tree-string',
'"' + value + '"');
}
if (t === 'number') {
return el('span', 'jf-tree-number',
String(value));
}
if (t === 'boolean') {
return el('span', 'jf-tree-boolean',
String(value));
}
// Object or Array
var isArr = Array.isArray(value);
var keys = isArr ? null : Object.keys(value);
var len = isArr ? value.length : keys.length;
if (len === 0) {
return el('span', 'jf-tree-bracket',
isArr ? '[]' : '{}');
}
var wrap = document.createElement('span');
wrap.style.display = 'block';
var icon = el('span',
'jf-tree-toggle-icon', '\u25bc');
var bracketOpen = el('span',
'jf-tree-bracket', isArr ? '[' : '{');
var toggle = document.createElement('span');
toggle.className = 'jf-tree-toggle';
toggle.appendChild(icon);
toggle.appendChild(bracketOpen);
var children = document.createElement('div');
children.className = 'jf-tree-children';
// ... render children recursively ...
toggle.addEventListener('click', function(e) {
e.stopPropagation();
var hidden =
children.style.display === 'none';
children.style.display =
hidden ? '' : 'none';
icon.textContent =
hidden ? '\u25bc' : '\u25b6';
icon.classList.toggle('collapsed', !hidden);
});
wrap.appendChild(toggle);
wrap.appendChild(children);
wrap.appendChild(el('span', 'jf-tree-bracket',
isArr ? ']' : '}'));
return wrap;
} function switchTab(name) {
document.querySelectorAll('.jf-tab')
.forEach(function(t) {
t.classList.toggle('active',
t.dataset.tab === name);
});
document.querySelectorAll('.jf-panel')
.forEach(function(p) {
p.classList.toggle('active',
p.id === 'jf' + name.charAt(0)
.toUpperCase() + name.slice(1) + 'Panel');
});
}
document.querySelectorAll('.jf-tab')
.forEach(function(tab) {
tab.addEventListener('click', function() {
switchTab(this.dataset.tab);
});
}); Color-coding by value type is the most impactful UX improvement. The human eye scans color faster than text. Follow the convention established by Chrome DevTools: strings = green, numbers = yellow, booleans = purple, null = gray.
Polish UX
Why learn this
A working tool is good. A polished tool is useful. Copy-to-clipboard, file download, live preview with debouncing, and clear error messages transform a demo into something people reach for every day. These features take minimal code but dramatically improve the user experience — they're the difference between a "neat experiment" and a "daily driver."
What you built and why this way
Copy to clipboard. navigator.clipboard.writeText() copies the formatted output. A button text change ("Copied!") provides confirmation. If clipboard API is unavailable, the fallback selects the output text and uses document.execCommand('copy').
Download as .json file. Blob + URL.createObjectURL creates a downloadable file with application/json MIME type. A dynamically created <a> element with download attribute triggers the save dialog. Same pattern used by all browser-based export tools.
Live preview with 300ms debounce. Every keystroke triggers a debounced re-parse after 300ms of inactivity. Faster than clicking "Format" manually and catches paste events. The 300ms delay balances responsiveness with performance. The debounce uses setTimeout/clearTimeout — no libraries needed.
Error highlighting with line/column display. When JSON is invalid, the error panel shows the full error message with position info. The error sticks until you fix the input. This is more useful than a simple alert — you can see the error alongside your input.
Key concepts
navigator.clipboard.writeText()— Async clipboard API. Returns a Promise. Rejects if permission deniedBlob+URL.createObjectURL()— Creates a downloadable file in memory. Revoke the URL after download to free memory- Debounce pattern —
clearTimeout(timer); timer = setTimeout(fn, delay). Ensures the function runs only after the user stops typing element.select()+document.execCommand('copy')— Legacy clipboard fallback
Alternative approaches
- Clipboard API with preserve format — Use
ClipboardItemto copy asapplication/jsonMIME type. Less browser support - Auto-download as .txt — Some users prefer .txt for cross-platform compatibility
- Throttle instead of debounce — Throttling runs at regular intervals regardless of input. Debounce is better for re-parsing
Browser compatibility
navigator.clipboard.writeText: Chrome 66+, Firefox 63+, Safari 13.1+. Falls back to execCommandBlob+URL.createObjectURL: Chrome 20+, Firefox 13+, Safari 8+, IE10+document.execCommand('copy'): Deprecated but still supported in all browsers
Performance notes
- 300ms debounce is the sweet spot. Below 150ms fires during fast typing. Above 500ms feels laggy
- Revoke old Blob URLs to prevent memory leaks on long-lived pages
Common pitfalls
- Clipboard promise not caught — Always chain a
.catch()that falls back to execCommand - Blob URL memory leak — Revoke after the download triggers with
URL.revokeObjectURL - Debounce on empty input — Check
if (!text.trim()) return;at the top - Error panel stuck open — Clear the error at the start of the update function, not at the end
All complete
The JSON Formatter is finished! All three steps are live in the interactive tool above. Next up on the roadmap: more developer utilities. See the full roadmap →
function fallbackCopy(text) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} catch (e) {}
ta.remove();
}
function downloadJSON(text) {
var blob = new Blob([text],
{ type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'formatted.json';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} var previewTimer;
input.addEventListener('input', function() {
clearTimeout(previewTimer);
previewTimer = setTimeout(function() {
var text = input.value.trim();
if (!text) return;
try {
var parsed = JSON.parse(text);
var indent = getIndent();
rawOutput.textContent =
JSON.stringify(parsed, null, indent);
buildTree(parsed);
clearError();
} catch (e) {
// Live preview hides errors silently
}
}, 300);
}); For the live preview, don't show parse errors on every keystroke — the user is in the middle of typing and the JSON is temporarily invalid. Only show errors from the explicit Validate button or Format button. This keeps the live preview helpful rather than annoying.
Lessons Learned — Build Process
The design insights, tradeoffs, and practical takeaways from building client-side JSON utility tools.
The native JSON API handles parsing, validation, and formatting in two function calls. No npm packages, no build steps, no WASM bundles. The space parameter in JSON.stringify accepts both numbers and strings, making tab-indented output trivial. For most JSON formatting needs, the browser's built-in API is all you need.
The tree view is a single recursive function that creates DOM elements directly. No virtual DOM, no diffing, no reconciliation. Each call handles one node type and returns a DOM element. The recursion mirrors the JSON structure exactly. This approach is easier to reason about than a state-managed component tree.
Zero server cost, zero data exposure, zero latency. Users can paste sensitive JSON (API keys, configs, personal data) without worrying about data leaving their machine. The tool works offline, loads instantly, and never breaks due to server downtime.
A formatter that silently fails on invalid input is useless. Clear, visible error messages with position info (line/column) turn a frustrating experience into a helpful one. The difference between "Invalid JSON" and "Unexpected token at line 3, column 12" is the difference between a dead end and a fixable problem.
Each step is a deliverable, not a milestone. Build it, verify it, ship it — then move to the next one. The JSON Formatter is built entirely with vanilla browser APIs, proving you don't need a framework to build polished interactive tools.