A JSON formatter can turn hard-to-read data into a consistent, indented structure, while a validator can tell you whether the input follows JSON syntax. Building one is a practical JavaScript project—and a useful way to learn what JSON.parse and JSON.stringify do, what errors they raise, and where their limits are.
In this tutorial, you’ll build a small formatter, connect it to a browser interface, and add tests. You’ll also learn why valid JavaScript object-literal syntax is not necessarily valid JSON, and what to consider when inputs are large or contain unusual numbers.
Key takeaways
- Use
JSON.parseto check whether a string is valid JSON and convert it into a JavaScript value. - Use
JSON.stringify(value, null, indent)to produce readable JSON. - Catch
SyntaxErrorand show its message, but don’t assume error wording or line-and-column details are consistent across browsers. - JavaScript object literals allow syntax that JSON does not, such as comments and unquoted property names.
- Parsing and re-serializing can change the representation of numbers and discard duplicate object keys.
How JSON formatting works
A formatter generally follows two steps: parse the input text into a JavaScript value, then serialize that value as JSON with the requested indentation.
JSON text → JSON.parse → JavaScript value → JSON.stringify → formatted JSON text
The second and third arguments to JSON.stringify are a replacer and a spacing option. Passing null for the replacer keeps the usual values, while a number for spacing adds that many spaces per indentation level. For example, 2 gives two-space indentation; 0 produces compact output. The numeric spacing option is limited to at most 10 spaces by the JavaScript specification.
A parse failure means the input is not valid JSON. The resulting SyntaxError can be used both to report invalid input and to prevent formatting from proceeding.
Build the formatter function
Save this as json-formatter.mjs. The function checks its inputs, parses the JSON, and returns the formatted string. It lets JSON.parse throw its SyntaxError so the caller can decide how to display the error.
export function formatJson(input, indent = 2) {
if (typeof input !== "string") {
throw new TypeError("Input must be a string.");
}
if (!Number.isInteger(indent) || indent < 0 || indent > 10) {
throw new RangeError("Indent must be an integer from 0 to 10.");
}
const value = JSON.parse(input);
return JSON.stringify(value, null, indent);
}
This function also accepts valid JSON whose top-level value is an array, string, number, boolean, or null. JSON does not require the top level to be an object. With an indentation value of 0, the result is valid but compact rather than visually indented.
Connect it to a browser interface
Add a text area, an indentation selector, a button, and separate areas for the result and status. Labels and a live status region make the small interface easier to use with assistive technology.
<label for="json-input">JSON input</label>
<textarea id="json-input" rows="12" spellcheck="false"></textarea>
<label for="indent">Indentation</label>
<select id="indent">
<option value="2">2 spaces</option>
<option value="4">4 spaces</option>
<option value="0">Compact</option>
</select>
<button id="format-button" type="button">Format and validate</button>
<p id="status" role="status" aria-live="polite"></p>
<pre id="output" aria-label="Formatted JSON"></pre>
Save the following as app.mjs alongside json-formatter.mjs. The handler catches parsing and input errors, reports them as text, and clears stale output after a failure. Using textContent avoids treating user-provided JSON as HTML.
import { formatJson } from "./json-formatter.mjs";
const input = document.querySelector("#json-input");
const indent = document.querySelector("#indent");
const button = document.querySelector("#format-button");
const status = document.querySelector("#status");
const output = document.querySelector("#output");
button.addEventListener("click", () => {
try {
output.textContent = formatJson(input.value, Number(indent.value));
status.textContent = "Valid JSON. Formatting complete.";
} catch (error) {
output.textContent = "";
status.textContent = error.message;
}
});
Serve the files from a local development server and load the page in a browser. ES modules are generally not loaded from a file:// URL in the same way as a page served over HTTP, so use your usual local server or development setup.
Test the formatter
Tests can check normal formatting, malformed input, and invalid indentation. Save this as json-formatter.test.mjs and run it with node --test json-formatter.test.mjs in a Node.js version that supports the built-in test runner.
import test from "node:test";
import assert from "node:assert/strict";
import { formatJson } from "./json-formatter.mjs";
test("formats an object with two-space indentation", () => {
const result = formatJson('{"name":"Ada","items":[1,2]}');
assert.equal(
result,
'{\n "name": "Ada",\n "items": [\n 1,\n 2\n ]\n}'
);
});
test("accepts a top-level JSON value", () => {
assert.equal(formatJson("true"), "true");
});
test("throws a syntax error for malformed JSON", () => {
assert.throws(() => formatJson('{"name":}'), SyntaxError);
});
test("rejects indentation outside the supported range", () => {
assert.throws(() => formatJson("{}", 11), RangeError);
});
These tests exercise the function’s output and error behavior without relying on the exact wording of a parser error, which can vary between JavaScript engines.
JSON is not JavaScript object-literal syntax
JSON is a data format with its own grammar. JavaScript object literals share some familiar punctuation, but they permit additional language features. For instance, this is a valid JavaScript object literal but not valid JSON:
const settings = {
theme: 'dark', // single quotes and a comment are allowed here
};
To represent the same data as JSON, property names and string values need double quotes, and comments and trailing commas must be removed:
{
"theme": "dark"
}
Other JavaScript values and constructs, such as undefined, functions, and NaN, are not JSON values. JSON.parse rejects JSON text that uses them. If a user pastes a JavaScript object literal into the formatter, explain that they need to convert it to JSON rather than suggesting that JSON parsing should accept the extra syntax.
Error messages and troubleshooting
When parsing fails, JSON.parse throws a SyntaxError. Displaying its message can help a user find a problem, but the message is provided by the JavaScript runtime. Its wording and any line or column information are not standardized across browsers. Avoid writing interface logic that extracts a location by parsing the error string unless you control and test the target runtimes.
Common causes of parse errors include:
- A missing comma between properties or array values.
- A trailing comma after the final property or item.
- A property name or string enclosed in single quotes.
- An unescaped quotation mark or backslash inside a string.
- A missing closing brace, bracket, or quotation mark.
- A comment or other JavaScript-only syntax in the input.
For a beginner-friendly interface, show the parser’s message and keep the original text in the input area so the user can correct it. Don’t silently remove punctuation or rewrite malformed input: automatic repairs can change the intended data.
Large inputs and data fidelity
JSON.parse and JSON.stringify run synchronously in the browser when called from a click handler. For sufficiently large input, parsing and serialization can make the page unresponsive. The amount of time and memory required depends on the input and the device, so avoid promising a particular size limit without testing your application’s expected workloads.
If large documents are part of the use case, consider measuring performance with representative inputs, showing progress or a clear busy state, and moving processing to a Web Worker so it does not block the main UI thread. A worker does not make the operation free: it still uses memory and takes time, and sending large strings between the page and worker has its own costs. Set product limits based on testing and explain them to users.
Parsing and serializing can also change details of the original text:
- Duplicate property names are not preserved as separate entries in the parsed object.
- Number spelling and formatting can change during serialization.
- JavaScript numbers cannot exactly represent every possible JSON integer. Very large integers can lose precision when parsed as
Numbervalues. - A valid JSON number with an extreme exponent can become
Infinityas a JavaScript number; serializing non-finite numeric values producesnull.
That means a JSON.parse/JSON.stringify formatter is suitable for many ordinary JSON documents, but it is not a byte-preserving editor. If exact number representation, duplicate keys, or original formatting must be preserved, use a parser and editing strategy designed to retain those details rather than round-tripping through ordinary JavaScript values.
Common implementation mistakes
- Using
evalto parse input: JSON parsing does not require executing code. UseJSON.parseinstead. - Assuming valid JavaScript means valid JSON: Object literals allow syntax JSON rejects. Validate against JSON syntax.
- Displaying user input with
innerHTML: Render text withtextContentto avoid interpreting input as markup. - Assuming error locations are consistent: Parser messages differ by runtime. Treat them as helpful diagnostics, not a stable data format.
- Calling every successful parse a lossless round trip: Parsing and serialization can change number representations and discard duplicate keys.
- Ignoring indentation input: Validate it before passing it to
JSON.stringifyso your function has a clear, predictable interface.
Conclusion
A JavaScript JSON formatter can be built with two standard methods: JSON.parse for validation and conversion, and JSON.stringify for readable output. A small input check, clear error handling, accessible status text, and tests make the project more reliable. Keep in mind that JSON has stricter syntax than JavaScript object literals, and that parsing and re-serializing are not always lossless—especially for duplicate keys and numbers outside JavaScript’s exact range.
