What You'll Build

A JSON formatter that takes messy or minified JSON, validates it, and outputs a clean, indented version. Includes error detection with line numbers, minify mode, and live stats showing character count.

  • Format — beautify JSON with 2-space indentation
  • Minify — strip all whitespace
  • Validate — check syntax, show errors with line/column
  • Live stats — characters, lines, and size in bytes
  • Copy output with one click
  • Sample JSON loaded as the default
▶ Live Preview — this is what you'll build

What You'll Learn

Why JSON matters: Every modern web API speaks JSON. If you're building a formatter that handles it, you're building a tool that developers will use daily. This is your first developer tool — the same pattern applies to regex testers, base64 encoders, and timestamp converters.

1

Build the HTML Structure

Create index.html. The layout is a textarea for input, a toolbar of buttons, an error/success area, and an output display.

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JSON Formatter</title>
</head>
<body>
    <div class="card">
        <h1>JSON Formatter</h1>
        <p class="sub">Format, validate, and minify JSON</p>

        <textarea id="input" spellcheck="false">{&quot;name&quot;:&quot;MAK Studio&quot;,&quot;tools&quot;:[&quot;JSON&quot;,&quot;BMI&quot;,&quot;Age&quot;],&quot;active&quot;:true}</textarea>

        <div class="actions">
            <button class="btn primary" id="formatBtn">Format</button>
            <button class="btn green" id="minifyBtn">Minify</button>
            <button class="btn amber" id="validateBtn">Validate</button>
            <button class="btn ghost" id="sampleBtn">Sample</button>
            <button class="btn ghost" id="clearBtn">Clear</button>
        </div>

        <div class="error" id="errorMsg"></div>
        <div class="success" id="successMsg"></div>

        <button class="copy-btn" id="copyBtn">Copy</button>
        <div class="output" id="output"></div>
        <div class="stats">
            <span>Input: <strong id="inLen">0</strong> chars</span>
            <span>Output: <strong id="outLen">0</strong> chars</span>
            <span>Size: <strong id="outSize">0 B</strong></span>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

Structure note: The output is a <div>, not a <textarea> or <pre>. This lets us use CSS to style it and prevents users from accidentally editing the formatted result.

2

Style It with CSS

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #f1f5f9;
    padding: 20px;
    color: #0f172a;
}

.card {
    max-width: 560px;
    margin: 0 auto;
    background: #fff;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}

h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }

/* Input textarea */
textarea {
    width: 100%;
    min-height: 140px;
    padding: 12px 14px;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    font-size: 0.85rem;
    font-family: "Courier New", monospace;
    outline: none;
    resize: vertical;
    line-height: 1.6;
    transition: 0.2s;
}
textarea:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

/* Toolbar */
.actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 12px 0;
}
.btn {
    border: none;
    border-radius: 8px;
    padding: 9px 14px;
    font-weight: 700;
    font-size: 0.8rem;
    cursor: pointer;
    transition: 0.2s;
    font-family: inherit;
}
.btn.primary { background: #3b82f6; color: #fff; }
.btn.primary:hover { background: #2563eb; }
.btn.green { background: #10b981; color: #fff; }
.btn.green:hover { background: #059669; }
.btn.amber { background: #f59e0b; color: #fff; }
.btn.amber:hover { background: #d97706; }
.btn.ghost { background: #f1f5f9; color: #334155; }
.btn.ghost:hover { background: #e2e8f0; }

/* Error + success messages */
.error {
    background: #fef2f2;
    border: 1px solid #fca5a5;
    color: #dc2626;
    padding: 10px 12px;
    border-radius: 8px;
    font-size: 0.8rem;
    font-family: "Courier New", monospace;
    margin: 8px 0;
    display: none;
}
.error.show { display: block; }

.success {
    background: #f0fdf4;
    border: 1px solid #86efac;
    color: #16a34a;
    padding: 10px 12px;
    border-radius: 8px;
    font-size: 0.8rem;
    font-weight: 700;
    margin: 8px 0;
    display: none;
}
.success.show { display: block; }

/* Output box — dark background like a terminal */
.output {
    background: #0f172a;
    border-radius: 10px;
    padding: 14px;
    color: #e2e8f0;
    font-family: "Courier New", monospace;
    font-size: 0.82rem;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 260px;
    overflow-y: auto;
    line-height: 1.5;
    min-height: 100px;
}
.output:empty::before {
    content: "Output will appear here...";
    color: #64748b;
    font-style: italic;
}

/* Stats row */
.stats {
    display: flex;
    gap: 12px;
    font-size: 0.72rem;
    color: #64748b;
    margin: 8px 0;
    flex-wrap: wrap;
}
.stats strong {
    color: #334155;
    font-family: "Courier New", monospace;
}

/* Copy button */
.copy-btn {
    background: transparent;
    border: 1px solid #334155;
    color: #94a3b8;
    padding: 4px 10px;
    border-radius: 6px;
    font-size: 0.72rem;
    cursor: pointer;
    float: right;
    font-family: inherit;
}
.copy-btn:hover { background: #334155; color: #fff; }
3

Write the JavaScript — Parse & Validate

The core function reads the input, tries to parse it, and either returns a parsed object or shows an error with line/column info.

JavaScript — Part 1: Parse
// ============================================================
// GRAB DOM ELEMENTS
// ============================================================

var inputEl = document.getElementById("input");
var outputEl = document.getElementById("output");
var errorEl = document.getElementById("errorMsg");
var successEl = document.getElementById("successMsg");
var inLenEl = document.getElementById("inLen");
var outLenEl = document.getElementById("outLen");
var outSizeEl = document.getElementById("outSize");

// ============================================================
// SAMPLE DATA — for the "Load Sample" button
// ============================================================

var SAMPLE = {
    name: "MAK Studio",
    tools: ["BMI Calculator", "Age Calculator", "JSON Formatter"],
    active: true,
    version: 2.1,
    meta: {
        author: "MAK Studio",
        url: "https://makuistudio.com"
    }
};

// ============================================================
// ERROR + SUCCESS HELPERS
// ============================================================

function showError(msg) {
    errorEl.textContent = "❌ " + msg;
    errorEl.classList.add("show");
    successEl.classList.remove("show");
}

function hideError() {
    errorEl.classList.remove("show");
}

function showSuccess(msg) {
    successEl.textContent = "✅ " + msg;
    successEl.classList.add("show");
    errorEl.classList.remove("show");
    // Auto-hide after 2.5s
    setTimeout(function() {
        successEl.classList.remove("show");
    }, 2500);
}

// ============================================================
// LINE/COLUMN EXTRACTION FROM ERROR POSITION
// ============================================================
// JSON.parse errors say "position N" where N is a character
// offset. We convert that to a human-readable line + column.

function getLineCol(text, pos) {
    var lines = text.slice(0, pos).split("\n");
    return {
        line: lines.length,
        col: lines[lines.length - 1].length + 1
    };
}

// ============================================================
// PARSE INPUT — returns parsed object or null
// ============================================================

function parseInput() {
    var text = inputEl.value.trim();

    if (!text) {
        showError("Please enter some JSON first.");
        return null;
    }

    try {
        return JSON.parse(text);
    } catch (e) {
        // Try to extract the error position for a better message
        var match = e.message.match(/position (\d+)/);

        if (match) {
            var pos = parseInt(match[1]);
            var lc = getLineCol(text, pos);
            showError("Invalid JSON at line " + lc.line +
                      ", column " + lc.col + " — " + e.message);
        } else {
            showError(e.message);
        }
        return null;
    }
}

Why try/catch is required: JSON.parse() throws an exception if the input is malformed. Without try/catch, your script would crash and no other code would run. Catching it lets you show a friendly error message instead.

4

Write the JavaScript — Format, Minify, Validate

JavaScript — Part 2: Actions
// ============================================================
// FORMAT — beautify with 2-space indentation
// ============================================================

document.getElementById("formatBtn").addEventListener("click", function() {
    var obj = parseInput();
    if (!obj) return;

    // The "2" means 2-space indentation
    outputEl.textContent = JSON.stringify(obj, null, 2);
    showSuccess("JSON formatted successfully.");
    updateStats();
});

// ============================================================
// MINIFY — remove all whitespace
// ============================================================

document.getElementById("minifyBtn").addEventListener("click", function() {
    var obj = parseInput();
    if (!obj) return;

    // No indentation argument = compact output
    outputEl.textContent = JSON.stringify(obj);
    showSuccess("JSON minified successfully.");
    updateStats();
});

// ============================================================
// VALIDATE — just check syntax
// ============================================================

document.getElementById("validateBtn").addEventListener("click", function() {
    var obj = parseInput();
    if (!obj) return;

    showSuccess("Valid JSON!");
});

// ============================================================
// SAMPLE, CLEAR, COPY
// ============================================================

document.getElementById("sampleBtn").addEventListener("click", function() {
    inputEl.value = JSON.stringify(SAMPLE, null, 2);
    outputEl.textContent = "";
    hideError();
    updateStats();
});

document.getElementById("clearBtn").addEventListener("click", function() {
    inputEl.value = "";
    outputEl.textContent = "";
    hideError();
    updateStats();
});

document.getElementById("copyBtn").addEventListener("click", function() {
    var text = outputEl.textContent;
    if (!text) {
        alert("Nothing to copy.");
        return;
    }
    navigator.clipboard.writeText(text).then(function() {
        showSuccess("Output copied!");
    });
});
5

Write the JavaScript — Stats & Boot

JavaScript — Part 3: Stats
// ============================================================
// STATS — character and byte counts
// ============================================================
// A Blob gives us an accurate byte count, since characters
// can be 1-4 bytes (UTF-8). This is more accurate than
// just using .length.

function updateStats() {
    var inLen = inputEl.value.length;
    var outLen = outputEl.textContent.length;
    var bytes = new Blob([outputEl.textContent]).size;

    inLenEl.textContent = inLen.toLocaleString();
    outLenEl.textContent = outLen.toLocaleString();
    outSizeEl.textContent = formatBytes(bytes);
}

function formatBytes(b) {
    if (b === 0) return "0 B";
    var k = 1024;
    var sizes = ["B", "KB", "MB"];
    var i = Math.floor(Math.log(b) / Math.log(k));
    return parseFloat((b / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
}

// ============================================================
// LIVE UPDATES
// ============================================================

inputEl.addEventListener("input", updateStats);

// Boot — set initial stats
updateStats();

Save and open index.html. Paste JSON, click Format — it beautifies. Break the JSON with a missing comma — you get an error with a line number. Try the Sample button.

6

Understand the Big Ideas

1. JSON.stringify's third argument

JSON.stringify(obj, null, 2) — the third argument is the indentation. Pass 2 for 2-space indentation, 4 for 4-space, or leave it out for compact output:

JavaScript
JSON.stringify({a: 1, b: 2}, null, 2);
// {
//   "a": 1,
//   "b": 2
// }

JSON.stringify({a: 1, b: 2});
// {"a":1,"b":2}

The second argument (null here) is a "replacer" — an optional function or array to filter which keys get included. We're not using it, but it's worth knowing for later.

2. Why JSON.parse errors give positions

When you call JSON.parse() on invalid JSON, the error message includes a character position: "Unexpected token } in JSON at position 42". We grab that number with a regex and convert it into a line/column, which is way more useful than a raw character offset.

3. Bytes vs. characters

"hello".length is 5. But "héllo".length is also 5 (JavaScript counts characters). The bytes — what gets sent over the network — differ, because "é" takes 2 bytes in UTF-8.

new Blob(["text"]).size gives the true byte count. This matters when you're optimizing API payloads.

4. Developer-tool UX patterns

Notice the design choices that make this feel like a pro tool:

7

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

Compare your solution: We built the production version with syntax highlighting, sorting, and more — see the live JSON Formatter.

Full Source Code

Common Mistakes & Fixes

❌ "Nothing happens when I click Format"

Check DevTools Console for errors. Most likely the script didn't load or there's a typo in an element ID. Every getElementById must match an actual ID in the HTML.

❌ "The line number in the error is wrong"

Browsers used to say "position" but newer ones say "at line X column Y" directly. If the regex doesn't match, we fall back to the raw message. Both are shown — use whichever is available.

❌ "Copy button doesn't work"

The Clipboard API requires HTTPS or localhost. If you're opening the file directly (file://), use python -m http.server to serve it locally.

❌ "Large JSON locks the browser"

For very large inputs (multi-megabyte JSON), JSON.parse and JSON.stringify are slow. Real tools add a "processing" state and use web workers. Not needed for learning, but good to know.

❌ "Trailing comma breaks parsing"

JSON is stricter than JavaScript. {"a": 1,} is invalid JSON, even though it works in JS. This is a common gotcha — JSON doesn't allow trailing commas.

What to Build Next