JavaScript JSON.parse() and JSON.stringify(): The Complete Guide for 2026
Every JavaScript developer uses JSON.parse() and JSON.stringify() on a near-daily basis, yet most never explore beyond the basic one-argument call. Both methods accept optional parameters that unlock powerful data transformations - and knowing about them can save you from writing a surprising amount of unnecessary glue code.
JSON.parse() and the Reviver Function
The basic usage is straightforward: pass a JSON string, get back a JavaScript value. But JSON.parse() takes a second argument - a reviver function that gets called for every key-value pair in the parsed result, bottom-up. This is your hook for transforming data as it comes in.
The most common use case is converting date strings back into Date objects:
const json = '{"name": "Alice", "joinedAt": "2025-09-15T10:30:00Z"}';
const user = JSON.parse(json, (key, value) => {
if (key === "joinedAt") {
return new Date(value);
}
return value;
});
console.log(user.joinedAt instanceof Date); // true
console.log(user.joinedAt.getFullYear()); // 2025The reviver receives every property, including nested ones. Returning undefinedfrom the reviver removes that property from the result entirely - handy for stripping out fields you don't need:
const data = JSON.parse(apiResponse, (key, value) => {
// Strip internal fields from the parsed output
if (key.startsWith("_")) return undefined;
return value;
});JSON.stringify() - Replacer and Space
JSON.stringify() is more versatile than most developers realize. It accepts three arguments: JSON.stringify(value, replacer, space).
The replacer can be either a function or an array. As an array, it acts as a whitelist of property names to include:
const user = {
id: 42,
name: "Bob",
email: "bob@example.com",
passwordHash: "abc123...",
role: "admin"
};
// Only include these specific fields
JSON.stringify(user, ["id", "name", "role"]);
// '{"id":42,"name":"Bob","role":"admin"}'As a function, the replacer works like the inverse of a reviver - you control what each value becomes in the output. This is useful for masking sensitive data:
JSON.stringify(user, (key, value) => {
if (key === "passwordHash") return undefined; // omit entirely
if (key === "email") return "[REDACTED]"; // mask
return value;
}, 2);The space argument controls indentation. Pass a number (up to 10) for that many spaces, or pass a string like "\t" for tab indentation. This is essentially what every JSON prettifier does under the hood - including ours.
Handling Parse Errors Gracefully
JSON.parse() throws a SyntaxError on invalid input. Never call it without error handling unless you are absolutely certain the input is valid JSON:
function safeParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch (err) {
console.error("Invalid JSON:", err.message);
return fallback;
}
}
const config = safeParse(rawInput, { theme: "default" });One common mistake: calling JSON.parse()on a value that's already a JavaScript object. If your API layer automatically parses responses, you'll get a confusing error about unexpected tokens. A quick guard function prevents this:
function ensureParsed(data) {
if (typeof data === "string") {
return JSON.parse(data);
}
return data;
}Deep Cloning (And Why You Should Use structuredClone() Instead)
For years, JSON.parse(JSON.stringify(obj)) was the go-to hack for deep cloning objects. It works because stringify serializes everything into a string, and parse creates entirely new objects from that string:
const original = { a: 1, b: { c: 2 } };
const clone = JSON.parse(JSON.stringify(original));
clone.b.c = 99;
console.log(original.b.c); // still 2But this approach has real limitations. It silently drops undefined values, functions, and Symbols. It converts Date objects to strings. NaN and Infinity become null. And it throws on circular references.
Modern JavaScript now has structuredClone(), which handles all of these edge cases properly. Use that instead for general-purpose deep cloning. The JSON round-trip technique still has value when you specifically want to strip non-serializable data - but it should no longer be your default.
The Date Problem
JSON has no native date type. When you stringify a Date, JavaScript calls its toISOString() method, producing a string like "2025-09-15T10:30:00.000Z". When you parse that JSON back, you get a plain string - not a Date object.
The reviver function is the standard fix. Here's a robust pattern that auto-detects ISO date strings:
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
function parseWithDates(json) {
return JSON.parse(json, (key, value) => {
if (typeof value === "string" && ISO_DATE_RE.test(value)) {
const date = new Date(value);
if (!isNaN(date.getTime())) return date;
}
return value;
});
}This pattern is simple but effective. It checks every string value against the ISO date regex and, if it matches, attempts to create a Date object. The isNaNguard ensures that strings that look like dates but aren't valid ones pass through unchanged.
Fetch API and JSON
The Fetch API has first-class JSON support through the response.json()method, which reads the body stream and parses it in one step. Here's the standard pattern for GET and POST:
// GET request - parsing JSON response
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const users = await response.json();
// POST request - sending JSON
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice", email: "alice@example.com" })
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const created = await res.json();Always check response.ok before calling .json(). A 500 error page might return HTML instead of JSON, and response.json() will throw a confusing parse error if it receives non-JSON content.
Performance Tips
A few things worth knowing when performance matters:
- JSON.parse() is fast.V8's JSON parser is heavily optimized. It's often faster to
JSON.parse()a large string literal than to write the equivalent JavaScript object literal, because the JSON grammar is simpler to parse. - Avoid repeated serialization. If you're calling
JSON.stringify()on the same object in a loop (for logging, caching, etc.), cache the result. - The array replacer is faster than a function replacer. When you just want to pick specific keys, use the array form. The engine can optimize the field lookup without calling a function for every property.
- Use streaming parsers for large files. When dealing with massive JSON files - hundreds of megabytes or more - loading everything into memory with
JSON.parse()is a bad idea. Node.js developers can reach for streaming parsers like stream-json to process records one at a time without holding the entire document in memory.
Quick Reference
Keep these JSON.stringify() behaviors in mind - they are the source of most serialization surprises:
undefined, functions, and Symbols are omitted from objects, converted tonullin arraysNaN,Infinity, and-Infinityall becomenullBigIntthrows aTypeError- you need a customtoJSON()or a replacerDateobjects serialize viatoISOString()MapandSetboth serialize to{}- you must convert them manually
Understanding these details turns JSON handling from a source of subtle bugs into a reliable, predictable part of your workflow. For a step-by-step guide to formatting JSON with different tools, see how to format JSON. And if you're building APIs that send and receive JSON, our guide to JSON in REST APIs covers best practices for structuring responses. You can also paste any payload into our formatter to debug it instantly with syntax highlighting and validation.
Inspect JSON Payloads Instantly
Need to inspect a JSON payload quickly? Paste it into our formatter for instant syntax highlighting and validation.
Open JSON Prettifier