JSON.parse converts a JSON string into a JavaScript value, and JSON.stringify converts a JavaScript value back into a JSON string — they are exact inverses of each other. You call JSON.stringify on the way out (saving to storage, sending over the network) and JSON.parse on the way in (reading a response, loading a config). Parsing turns text you cannot use directly into a live object; stringifying turns a live object into text you can store or transmit. The two together are the entire bridge between JavaScript's in-memory world and the flat text that travels between systems.
That is the summary an AI overview will give you. The part it can't give you is the set of quiet traps that bite real code: stringify silently deletes your undefined values and functions, turns Date objects into strings you can't turn back, converts NaN to null, and throws on a circular reference or a BigInt. This article shows the full comparison, the round-trip, and each gotcha with the code to handle it.
The round trip in one picture
Think of the two methods as a two-way door between a structured object living in memory and a flat string of text. stringify flattens; parse rebuilds.
The direction you're moving decides which method you reach for first. Producing JSON (saving, sending)? stringify first. Consuming JSON (reading, loading)? parse first.
JSON.parse vs JSON.stringify: the full comparison
| JSON.parse | JSON.stringify | |
|---|---|---|
| Direction | JSON string → JS value | JS value → JSON string |
| Signature | JSON.parse(text[, reviver]) | JSON.stringify(value[, replacer[, space]]) |
| Input | A string of valid JSON | Any JavaScript value |
| Output | Object, array, number, string, boolean, or null | A string (or undefined for unsupported top-level values) |
| Optional hook | reviver — transform each value on the way in | replacer — filter/transform each value on the way out |
| Formatting control | None | space — pretty-print with N spaces or a string |
| On bad input | Throws SyntaxError immediately | Silently omits undefined/functions; throws on circular refs & BigInt |
| Typical use | Reading a fetch response, loading config, localStorage.getItem | Sending a request body, localStorage.setItem, logging |
| Which do I use? | When you receive text and need an object | When you have an object and need text |
JSON.parse: string → object
JSON.parse takes JSON text and returns the corresponding JavaScript value. The single most important thing to know: it throws on invalid input, so untrusted or network data belongs in a try/catch.
const obj = JSON.parse('{"name":"Ada","age":36}');
obj.name; // "Ada" — now a real object you can read and mutate
// Malformed JSON throws — always guard external input
try {
JSON.parse("{ name: 'Ada' }"); // single quotes + unquoted key = invalid
} catch (err) {
console.error("Bad JSON:", err.message);
}
The optional reviver is a function called for every key/value pair after parsing, letting you transform values on the way in — most commonly to rebuild Date objects (see the Dates section). Returning undefined from the reviver removes that property from the result.
const data = JSON.parse('{"createdAt":"2026-07-18T00:00:00.000Z"}', (key, value) => {
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value); // turn ISO strings back into Date objects
}
return value;
});
data.createdAt instanceof Date; // true
JSON.stringify: object → string
JSON.stringify walks a JavaScript value and returns JSON text. Where parse fails loudly, stringify mostly fails quietly — it drops values it can't represent rather than erroring, which is exactly why the gotchas below catch people.
JSON.stringify({ name: "Ada", age: 36 });
// '{"name":"Ada","age":36}'
// space (3rd arg) pretty-prints — great for files and logs, never for payloads
JSON.stringify({ name: "Ada", age: 36 }, null, 2);
// {
// "name": "Ada",
// "age": 36
// }
The optional replacer is either an allow-list array of property names, or a function run for every value on the way out. Returning undefined from a replacer function drops that property.
// Array replacer: keep only these keys
JSON.stringify({ name: "Ada", age: 36, secret: "x" }, ["name", "age"]);
// '{"name":"Ada","age":36}'
// Function replacer: redact a field, double every number
JSON.stringify({ price: 10, token: "abc" }, (key, value) => {
if (key === "token") return undefined; // drop it
if (typeof value === "number") return value * 2;
return value;
});
// '{"price":20}'
The gotchas an AI overview skips
This is the part that separates "I know what the two methods do" from "I know why my data came back wrong." All of these are JSON.stringify behaviors, because JSON is a much smaller type system than JavaScript — several JS values have no JSON equivalent, so something has to give.
| JavaScript value | What JSON.stringify does | Why |
|---|---|---|
undefined (in object) | Property omitted entirely | Not valid JSON |
undefined (in array) | Replaced with null | Array positions must stay aligned |
| Function / Symbol value | Omitted (object) or null (array) | Not valid JSON |
| Symbol-keyed property | Ignored, even by a replacer | Symbols aren't serializable |
NaN, Infinity, -Infinity | Become null | JSON has no non-finite numbers |
new Date() | ISO-8601 string via toJSON() | JSON has no date type |
BigInt | Throws TypeError | Intentional — no safe representation |
| Circular reference | Throws TypeError | Can't serialize an infinite loop |
Map, Set | Become {} (empty object) | No JSON equivalent; not enumerable as props |
JSON.stringify({ a: undefined, b: () => {}, c: NaN, d: [undefined, 1] });
// '{"c":null,"d":[null,1]}' — a and b vanished; NaN → null; array undefined → null
JSON.stringify({ big: 10n }); // TypeError: Do not know how to serialize a BigInt
const node = {};
node.self = node;
JSON.stringify(node); // TypeError: Converting circular structure to JSON
Dates don't survive a round trip
A Date stringifies to an ISO string (via its built-in toJSON), and parse has no idea it was ever a date. The round trip is lossy unless you rebuild it with a reviver:
const start = { at: new Date("2026-07-18") };
const text = JSON.stringify(start); // '{"at":"2026-07-18T00:00:00.000Z"}'
const back = JSON.parse(text);
typeof back.at; // "string" — NOT a Date
// Rebuild with a reviver:
const fixed = JSON.parse(text, (k, v) =>
typeof v === "string" && /^\d{4}-\d{2}-\d{2}T/.test(v) ? new Date(v) : v
);
fixed.at instanceof Date; // true
Circular references: handle or avoid
For a cyclic graph, either strip the cycle with a WeakSet-based replacer, or reach for the modern built-in structuredClone() when you actually want a copy rather than a string.
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) return undefined; // drop the second sighting
seen.add(value);
}
return value;
});
}
The deep-clone shortcut (and its trap)
You'll see JSON.parse(JSON.stringify(obj)) used as a quick deep copy. It genuinely works — but only for plain JSON-safe data. Run any rich object through it and the gotchas above quietly corrupt your copy: Dates turn to strings, undefined and functions disappear, Map/Set empty out, NaN becomes null, and a BigInt throws.
const clone = JSON.parse(JSON.stringify(original)); // fine for nested plain objects/arrays
const faithful = structuredClone(original); // preserves Dates, Maps, Sets, undefined, etc.
If the object is pure data, the JSON trick is fine and fast. If it holds Dates, Maps, Sets, or undefined you care about, use structuredClone() — it exists precisely for the cases where the JSON round trip lies to you.
When you use each in practice
- Sending an API request —
fetch(url, { body: JSON.stringify(payload) }). Stringify out. - Reading an API response —
const data = await res.json()(which parses for you), orJSON.parse(text). Parse in. - localStorage / sessionStorage — storage only holds strings:
setItem(k, JSON.stringify(v))to save,JSON.parse(getItem(k))to read. - Config & log files —
JSON.parseto load;JSON.stringify(obj, null, 2)to write something a human can read. - Pretty-printing for humans — the
spaceargument; use a live JSON formatter to see the difference instantly.
The bottom line
JSON.parse and JSON.stringify are two halves of one bridge: parse brings flat text into JavaScript as a live value, stringify sends a live value back out as text. Remember the direction (produce → stringify, consume → parse), always wrap parse in try/catch because it throws on bad input, and respect the type gap — stringify will quietly drop undefined, functions, and symbols, flatten Dates to strings, turn NaN into null, and throw on BigInt and circular references. Reach for the reviver, replacer, and space parameters when the defaults aren't enough, and reach for structuredClone() when you need a faithful copy rather than a string.