A step-by-step guide to building a fully functional JSON formatter, validator, and minifier — zero dependencies.
JSON is the lingua franca of the web. Every API returns it, every config file uses it, and every developer has spent time staring at a wall of unreadable JSON trying to find the missing comma.
Most JSON formatters online are either bloated web apps full of ads, or they send your data to a remote server. Building your own gives you:
This tutorial teaches you about error handling, string manipulation, and the core JSON API that powers every modern web application.
A JSON formatter needs an input area, an output area, and action buttons. Here's the structure:
HTML"><div class="json-formatter">
<!-- Action buttons -->
<div class="toolbar">
<button id="format-btn">Format</button>
<button id="minify-btn">Minify</button>
<button id="copy-btn">Copy</button>
<button id="clear-btn">Clear</button>
</div>
<!-- Input/Output panels -->
<div class="panels">
<div class="panel">
<label>Input JSON</label>
<textarea id="input" placeholder='{"key": "value"}'></textarea>
</div>
<div class="panel">
<label>Output</label>
<pre id="output"></pre>
</div>
</div>
<!-- Status bar -->
<div id="status" class="status">Enter JSON above and click Format</div>
</div>
Key design decisions:
Every JSON operation starts with parsing. JSON.parse() either returns your data or throws an error:
JavaScript">// Core parsing function — returns { success, data, error }
function parseJSON(input) {
try {
const data = JSON.parse(input);
return { success: true, data: data, error: null };
} catch (error) {
return {
success: false,
data: null,
error: error.message
};
}
}
// Usage
const result = parseJSON('{"name": "K1R4", "version": 1}');
// result = { success: true, data: { name: "K1R4", version: 1 }, error: null }
const badResult = parseJSON('{name: K1R4}');
// badResult = { success: false, data: null, error: "Unexpected token n..." }
Key insight: JSON.parse() is synchronous and throws on invalid JSON. The try/catch pattern is the only way to handle invalid input gracefully. Always wrap it in try/catch — never assume the input is valid.
Beautifying JSON is deceptively simple: parse it, then stringify with indentation:
JavaScript">// Beautify: parse then re-stringify with indentation
function beautifyJSON(input, indent = 2) {
const result = parseJSON(input);
if (!result.success) {
throw new Error(result.error);
}
return JSON.stringify(result.data, null, indent);
}
// Example:
const raw = '{"users":[{"name":"Alice"},{"name":"Bob"}]}';
beautifyJSON(raw, 2);
// Returns:
// {
// "users": [
// {
// "name": "Alice"
// },
// {
// "name": "Bob"
// }
// ]
// }
// With 4-space indentation:
beautifyJSON(raw, 4);
// Returns:
// {
// "users": [
// {
// "name": "Alice"
// },
// {
// "name": "Bob"
// }
// ]
// }
The second argument to JSON.stringify() — null — is the replacer parameter. When it's null, all properties are included. You can pass a function or array to filter specific keys.
The third argument to JSON.stringify() controls indentation. Use 2 spaces for web APIs (smaller payload), 4 spaces for readability (code reviews, config files). The default is 2.
Minifying is the reverse: parse then stringify with no whitespace:
JavaScript">// Minify: parse then re-stringify with zero whitespace
function minifyJSON(input) {
const result = parseJSON(input);
if (!result.success) {
throw new Error(result.error);
}
return JSON.stringify(result.data);
}
// Example:
const pretty = JSON.stringify({
users: [
{ name: "Alice" },
{ name: "Bob" }
]
}, null, 2);
// pretty = '{\n "users": [\n {\n "name": "Alice"\n },\n {\n "name": "Bob"\n }\n ]\n}'
minifyJSON(pretty);
// Returns: '{"users":[{"name":"Alice"},{"name":"Bob"}]}'
Minification removes all whitespace, newlines, and indentation. The resulting string is functionally identical but significantly smaller — important for API payloads and storage.
A formatted JSON output is much easier to read with syntax highlighting. Here's a lightweight approach using regex:
JavaScript">// Lightweight JSON syntax highlighter
function highlightJSON(jsonString) {
// Escape HTML entities first
let escaped = jsonString
.replace(/&/g, '&')
.replace(//g, '>');
// Apply color classes via regex
// Strings: "value"
escaped = escaped.replace(
/"([^"]*)"(?=:)/g,
'<span class="json-key">"$1"</span>:'
);
// String values
escaped = escaped.replace(
/:"([^"]*)"/g,
': <span class="json-string">"$1"</span>'
);
// Numbers
escaped = escaped.replace(
/: (\d+\.?\d*)/g,
': <span class="json-number">$1</span>'
);
// Booleans and null
escaped = escaped.replace(
/: (true|false|null)/g,
': <span class="json-boolean">$1</span>'
);
return escaped;
}
// CSS for the highlight classes:
// .json-key { color: #8b5cf6; }
// .json-string { color: #22c55e; }
// .json-number { color: #3b82f6; }
// .json-boolean { color: #f59e0b; }
Regex-based highlighting is fast but not perfect. It won't handle nested objects inside strings, escaped quotes, or edge cases. For production use, consider a dedicated library. For a personal tool, this approach is more than adequate.
A JSON formatter is only useful if it tells users what went wrong when they paste invalid JSON. Here's the complete event handling:
JavaScript">// DOM references
const inputEl = document.getElementById('input');
const outputEl = document.getElementById('output');
const statusEl = document.getElementById('status');
// Format button handler
document.getElementById('format-btn').addEventListener('click', () => {
const input = inputEl.value.trim();
if (!input) {
setStatus('Please enter some JSON', 'error');
return;
}
const result = parseJSON(input);
if (result.success) {
outputEl.textContent = JSON.stringify(result.data, null, 2);
setStatus('Valid JSON ✓', 'success');
} else {
outputEl.textContent = '';
setStatus(`Error: ${result.error}`, 'error');
}
});
// Minify button handler
document.getElementById('minify-btn').addEventListener('click', () => {
const input = inputEl.value.trim();
if (!input) {
setStatus('Please enter some JSON', 'error');
return;
}
const result = parseJSON(input);
if (result.success) {
outputEl.textContent = JSON.stringify(result.data);
setStatus('Minified ✓', 'success');
} else {
outputEl.textContent = '';
setStatus(`Error: ${result.error}`, 'error');
}
});
// Copy button handler
document.getElementById('copy-btn').addEventListener('click', async () => {
const text = outputEl.textContent;
if (!text) {
setStatus('Nothing to copy', 'warning');
return;
}
try {
await navigator.clipboard.writeText(text);
setStatus('Copied to clipboard!', 'success');
} catch (err) {
setStatus('Failed to copy', 'error');
}
});
// Clear button handler
document.getElementById('clear-btn').addEventListener('click', () => {
inputEl.value = '';
outputEl.textContent = '';
setStatus('Cleared', 'info');
});
// Status helper
function setStatus(message, type = 'info') {
statusEl.textContent = message;
statusEl.className = `status status-${type}`;
}
Good UX in a JSON formatter means:
Here's everything assembled into a single, working HTML file:
index.html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSON Formatter</title>
<style>
/* Paste the CSS here */
</style>
</head>
<body>
<div class="json-formatter">
<!-- Paste the HTML from Step 1 here -->
</div>
<script>
// Paste the JavaScript from Steps 2-6 here
</script>
</body>
</html>
The full, working implementation is available as a single HTML file — no build step required. It includes:
The fully working JSON Formatter is live and free to use:
Open JSON Formatter → View This TutorialAll tools on k1r4.space are free, open-source, and require no signup, tracking, or ads. They're built with vanilla HTML, CSS, and JavaScript — no frameworks, no build tools.
Building a JSON formatter taught us several important concepts: