To parse JSON in JavaScript, call JSON.parse. To turn a value back into JSON text, call JSON.stringify. These two built-in functions are all you need to move between JSON text and live objects. Both accept optional callbacks that most developers never use, and both have rough edges around dates, BigInt, circular references and values that quietly disappear.
This guide covers the whole API: the reviver and replacer callbacks, the space argument for pretty-printing, toJSON(), why dates need special handling, the limits of a JSON round trip for deep cloning, and how to parse untrusted input without crashing your app. Every section has runnable code.
The two functions behind JSON in JavaScript
Working with JSON in JavaScript comes down to two methods on the global JSON object. JSON.parse(text) reads a JSON string and returns the matching JavaScript value: an object, array, string, number, boolean or null. This is what other languages call decoding JSON. JSON.stringify(value) does the reverse and serializes a value into a JSON string. Neither method touches the network or the filesystem. Both are synchronous conversions between text and in-memory data.
const text = '{"name":"Ada","active":true,"score":42}';
const obj = JSON.parse(text);
console.log(obj.name); // "Ada"
console.log(obj.score); // 42 (a real number, not a string)
const backToText = JSON.stringify(obj);
console.log(backToText); // {"name":"Ada","active":true,"score":42}That covers most everyday use. Bugs hide in the rest: the optional second and third arguments, and the types JSON cannot represent. If you are new to the format, start with what is JSON and the JSON data types guide, then come back here.
JSON.parse and the reviver callback
JSON.parse takes an optional second argument called the reviver. It is a function (key, value) => newValue that runs for every key/value pair, starting from the innermost values. Return the value unchanged to keep it, return something new to transform it, or return undefined to drop the property. The reviver is the standard place to restore types that JSON has no syntax for.
Turning date strings back into Date objects
JSON has no date type, so dates are almost always serialized as ISO 8601 strings. When you parse them back, they are still strings. A reviver can detect them and turn them into Date objects:
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
function dateReviver(key, value) {
if (typeof value === "string" && ISO_DATE.test(value)) {
return new Date(value);
}
return value;
}
const json = '{"title":"Launch","start":"2026-07-12T09:00:00.000Z"}';
const event = JSON.parse(json, dateReviver);
console.log(event.start instanceof Date); // true
console.log(event.start.getFullYear()); // 2026The key argument is the property name, or the array index as a string. The top-level value comes last, with an empty string as its key. The reviver works from the bottom up, so nested objects are already transformed when their parent is visited.
JSON.stringify: space, replacer, and toJSON()
JSON.stringify(value, replacer, space) takes two optional arguments that control what gets serialized and how it is formatted.
The space argument for pretty-printing
Pass a number to indent with that many spaces (values above 10 are capped at 10), or a string to indent with that string. A JSON formatter does the same thing.
const config = { host: "localhost", port: 8080, tls: false };
console.log(JSON.stringify(config, null, 2));
// {
// "host": "localhost",
// "port": 8080,
// "tls": false
// }
console.log(JSON.stringify(config, null, "\t")); // tab-indentedThe replacer: filtering and transforming
The replacer mirrors the reviver but runs during serialization. As a function (key, value) => newValue, it can omit or rewrite values. Returning undefined drops the property. As an array of keys, it acts as an allowlist of properties to keep, which helps when stripping sensitive fields.
const user = { id: 1, name: "Ada", password: "secret", token: "abc" };
// Function form: redact sensitive keys
const safe = JSON.stringify(user, (key, value) =>
key === "password" || key === "token" ? undefined : value
);
console.log(safe); // {"id":1,"name":"Ada"}
// Array form: keep only these keys
console.log(JSON.stringify(user, ["id", "name"]));
// {"id":1,"name":"Ada"}toJSON(): let an object serialize itself
If a value has a toJSON() method, JSON.stringify calls it and serializes the return value instead of the object. That is why dates "just work" when you stringify them: Date.prototype.toJSON returns an ISO string. You can add the same hook to your own classes:
class Money {
constructor(cents) { this.cents = cents; }
toJSON() { return (this.cents / 100).toFixed(2); }
}
console.log(JSON.stringify({ price: new Money(1999) }));
// {"price":"19.99"}What JSON.stringify silently drops or breaks
JSON.stringify cannot represent every JavaScript value, and it fails in different ways. Some values vanish quietly. Others throw. Knowing which is which prevents data loss bugs that only show up in production.
| Value type | What happens |
|---|---|
undefined | Dropped from objects, becomes null inside arrays |
| Function | Dropped from objects, becomes null inside arrays |
Symbol | Dropped entirely |
NaN, Infinity | Serialized as null |
BigInt | Throws a TypeError |
| Circular reference | Throws a TypeError |
Date | Becomes an ISO string (via toJSON) |
Map, Set | Becomes {} and the contents are lost |
// BigInt -> TypeError: Do not know how to serialize a BigInt
try {
JSON.stringify({ big: 9007199254740993n });
} catch (e) {
console.log(e.name); // "TypeError"
}
// Circular reference -> TypeError
const node = { name: "a" };
node.self = node;
try {
JSON.stringify(node);
} catch (e) {
console.log(e.name); // "TypeError"
}
// A replacer can serialize BigInt as a string
const out = JSON.stringify({ big: 42n }, (key, value) =>
typeof value === "bigint" ? value.toString() : value
);
console.log(out); // {"big":"42"}The BigInt limit matters when parsing too. JSON numbers larger than Number.MAX_SAFE_INTEGER (2^53 - 1) lose precision when read by JSON.parse, because they become IEEE-754 doubles. If your API returns 64-bit IDs, send them as strings.
Deep copies: structuredClone vs the JSON round trip
For years the usual trick for deep-cloning a plain object was JSON.parse(JSON.stringify(obj)). It works for JSON-safe data, but it has every limitation above. Dates become strings, Map and Set become empty objects, undefined and functions disappear, and BigInt or circular references throw. Modern JavaScript has a built-in alternative, structuredClone(), in all current browsers and in Node 17+.
const original = {
when: new Date("2026-01-01"),
tags: new Set(["a", "b"]),
note: undefined,
};
// JSON round-trip: lossy
const viaJson = JSON.parse(JSON.stringify(original));
console.log(viaJson.when instanceof Date); // false (it's a string)
console.log(viaJson.tags); // {} (Set is gone)
console.log("note" in viaJson); // false (undefined dropped)
// structuredClone: type-preserving
const clone = structuredClone(original);
console.log(clone.when instanceof Date); // true
console.log(clone.tags instanceof Set); // true
console.log(clone.when === original.when); // false (a real copy)Fetching and safely parsing JSON
Most JSON in a real app arrives over the network. The fetch API gives you a Response with a .json() method. It reads the body, runs JSON.parse for you and returns a promise. The promise can reject on a network failure or on a body that is not valid JSON, so always handle errors.
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`Request failed: ${res.status}`);
}
// res.json() rejects if the body isn't valid JSON
return res.json();
}
getUser(1)
.then((user) => console.log(user.name))
.catch((err) => console.error("Could not load user:", err));When you call JSON.parse on a string from localStorage, a query param, a message event or a file, invalid input throws a SyntaxError. Never assume a stored or incoming string is well-formed. A small wrapper keeps the failure local instead of crashing the caller:
function safeParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
const prefs = safeParse(localStorage.getItem("prefs"), {});
console.log(prefs); // {} if the stored value was missing or corruptWhen you debug a stringified payload by hand, a formatter makes the structure readable. It does what JSON.stringify(value, null, 2) does, plus validation and syntax highlighting. For production payloads, the JSON Minifier does the opposite.