JSON.stringify vs JSON.parse in JavaScript
Photo by Lautaro Andreani on Unsplash
Table of Contents
The Two Directions, Defined
These two functions are the workhorses of every JavaScript app that talks to an API, saves to localStorage, or logs structured data. They look simple, and the happy path is. But the edge cases — what happens to dates, undefined values, circular references, and special number types — cause real bugs that are hard to spot because `stringify` fails silently rather than throwing. I've lost afternoons to a field that quietly disappeared on the way to the server, and every time it traced back to one of the rules below.
This post explains exactly what each function does, walks through the data types that don't survive the round trip, shows the optional arguments most people never use (the replacer, the reviver, the indent), and covers the practical patterns: pretty-printing, deep copying, and safe parsing. By the end you'll know precisely what comes out the other side.
JSON.stringify — Object to String
```javascript const user = { name: 'Ava', age: 30, active: true }; JSON.stringify(user); // '{"name":"Ava","age":30,"active":true}' ```
That string is what you put in a `fetch` POST body, write to a file, or stash in localStorage. Strings, numbers, booleans, `null`, arrays, and plain objects all serialize cleanly.
What doesn't survive is the part that bites people:
```javascript JSON.stringify({ a: undefined, // dropped entirely b: function() {}, // dropped entirely c: Symbol('x'), // dropped entirely d: NaN, // becomes null e: Infinity, // becomes null f: new Date() // becomes an ISO string }); // '{"d":null,"e":null,"f":"2026-06-20T12:46:00.000Z"}' // note: a, b, and c vanished completely ```
Key rules:
- `undefined`, functions, and symbols are dropped from objects entirely (in arrays, they become `null` to preserve indices). - `NaN` and `Infinity` become `null` because JSON has no representation for them. - `Date` objects become ISO 8601 strings via their `toJSON` method — they do not come back as Dates when you parse. - Circular references throw a `TypeError`. If object A references B and B references A, `stringify` can't represent that and gives up.
The silent dropping is the dangerous one. You stringify an object, half its fields are gone, and there's no error — you just get malformed-looking data downstream. If you've ever wondered why a field vanished between your client and your API, this is usually why. The MDN JSON.stringify reference documents every rule precisely. And when the resulting JSON looks wrong, paste it into the JSON formatter and validator to see the actual structure that came out.
JSON.parse — String to Object
Photo by John Schnobrich on Unsplash
```javascript const text = '{"name":"Ava","age":30}'; const obj = JSON.parse(text); obj.name; // 'Ava' obj.age; // 30 (a real number, not a string) ```
The critical thing to internalize: parse is strict. Unlike JavaScript object literals, JSON requires double quotes on every key and string, forbids trailing commas, forbids comments, and forbids single quotes. Any of these throws a `SyntaxError`:
```javascript JSON.parse("{'name': 'Ava'}"); // SyntaxError — single quotes JSON.parse('{"name": "Ava",}'); // SyntaxError — trailing comma JSON.parse('{name: "Ava"}'); // SyntaxError — unquoted key JSON.parse('{"name": "Ava" // hi }'); // SyntaxError — comments ```
These are the same strictness rules a JSON validator enforces, which is why pasting a failing string into one immediately shows you the offending character. The error message JavaScript gives you ("Unexpected token } in JSON at position 23") points at a position, but a formatter shows it visually.
Because parse throws on bad input, always wrap it when the source is untrusted — an API response, user input, a file:
```javascript function safeParse(text, fallback = null) { try { return JSON.parse(text); } catch { return fallback; } } ```
This pattern saves you from a single malformed response crashing the whole flow. It's especially important when reading from a network, where a server returning an HTML error page instead of JSON is common — exactly the failure I cover in reading JSON from an API. One more note: `JSON.parse` does not revive Dates. A string that looks like an ISO date stays a string; if you need a real Date back, convert it explicitly with the reviver argument or after parsing.
The Optional Arguments Nobody Uses
stringify's third argument: indentation. This is the one everyone should know:
```javascript JSON.stringify(obj, null, 2); // indent with 2 spaces JSON.stringify(obj, null, '\t'); // indent with tabs ```
The `2` (or a tab character) turns minified JSON into readable, pretty-printed JSON with line breaks and indentation. This is what every "pretty print JSON" tool does under the hood. It's invaluable for logging, debugging, and writing config files by hand.
stringify's second argument: the replacer. Either an array of keys to keep, or a function to transform values:
```javascript // keep only specific keys JSON.stringify(user, ['name', 'age']);
// transform values — e.g. redact a password JSON.stringify(user, (key, value) => key === 'password' ? '[REDACTED]' : value ); ```
The function replacer runs for every key/value pair, letting you filter, redact, or convert as you serialize. Redacting secrets before logging is a great use.
parse's second argument: the reviver. The mirror of the replacer — a function that transforms values as they're parsed:
```javascript const obj = JSON.parse(text, (key, value) => { // revive ISO date strings back into Date objects if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) { return new Date(value); } return value; }); ```
This is the standard fix for the "dates don't come back" problem — detect the ISO string pattern and rehydrate it into a real `Date`. Most people write a separate post-processing loop for this; the reviver does it inline during parse, which is cleaner and faster.
The Patterns You'll Actually Reach For
Photo by Glenn Carstens-Peters on Unsplash
Pretty-print for logging and config:
```javascript console.log(JSON.stringify(data, null, 2)); ```
Reading a 2-space-indented object in your console beats squinting at a single-line blob. I use this reflexively when debugging API responses.
Deep copy (with caveats):
```javascript const copy = JSON.parse(JSON.stringify(original)); ```
This creates a deep clone of plain-data objects — nested objects and arrays are fully duplicated, not referenced. The caveats are the stringify rules: this copy loses functions, undefined, dates-as-dates, and chokes on circular references. For pure JSON-shaped data it's a quick, dependency-free deep copy. For objects with methods or Dates, use `structuredClone()` (built into modern browsers) instead, which handles more types.
localStorage round trip:
```javascript // save localStorage.setItem('settings', JSON.stringify(settings)); // load const settings = JSON.parse(localStorage.getItem('settings') || '{}'); ```
localStorage only stores strings, so every object you persist has to be stringified going in and parsed coming out. The `|| '{}'` guards against `null` when the key doesn't exist yet. This pattern is everywhere in client-side state; my localStorage vs sessionStorage guide covers when to use each store.
Comparing two objects for equality (rough):
```javascript JSON.stringify(a) === JSON.stringify(b); ```
This works for simple cases but is fragile — key order matters, so `{x:1,y:2}` and `{y:2,x:1}` stringify differently despite being "equal." Use it only for quick checks on objects you control the shape of, never as a real deep-equality function.
Serializing a Map or Set (they don't work directly):
```javascript const m = new Map([['a', 1], ['b', 2]]); JSON.stringify(m); // '{}' — Maps serialize to empty objects! ```
This one catches everyone at least once. `Map` and `Set` don't have a JSON representation, so `stringify` returns `{}` for a Map and `[]` for a Set — silently losing all your data. The fix is to convert them first: `JSON.stringify([...map])` turns the Map into an array of `[key, value]` pairs that survives the round trip, and `JSON.parse` plus `new Map(parsed)` rebuilds it on the other side. I got burned by this storing a Map of feature flags in localStorage and reading back an empty object the next session.
Custom serialization with `toJSON`:
```javascript class Money { constructor(cents) { this.cents = cents; } toJSON() { return { dollars: this.cents / 100 }; } } JSON.stringify({ price: new Money(1599) }); // '{"price":{"dollars":15.99}}' ```
If an object has a `toJSON` method, `stringify` calls it and serializes the return value instead of the object itself. This is exactly how `Date` becomes an ISO string. You can use the same hook to control how your own classes serialize — handy for hiding internal fields or formatting values on the way out, without touching your serialization call sites.
When any of these produce JSON that looks off, I drop it straight into the JSON formatter rather than debugging by eye — it validates the structure and pretty-prints it in one step, which has caught more than one stray trailing comma or mis-nested bracket for me before it reached an API. The habit of validating serialized output before it leaves the function has saved me from shipping silently-broken payloads more than once, and it costs all of five seconds.
Frequently Asked Questions
What's the difference between JSON.stringify and JSON.parse?
JSON.stringify converts a JavaScript value (object, array, number) into a JSON string — the format you send over a network or save to storage. JSON.parse does the reverse, turning a JSON string back into a live JavaScript value. They're inverses, so JSON.parse(JSON.stringify(obj)) produces a deep copy of plain-data objects. You can format and validate the JSON between them with the [ToolsFuel JSON formatter](/tools/json-formatter).
How do I pretty-print JSON in JavaScript?
Pass a third argument to JSON.stringify: JSON.stringify(obj, null, 2) indents with 2 spaces, and JSON.stringify(obj, null, '\t') uses tabs. The second argument (null here) is the replacer, which you can skip. This adds line breaks and indentation, turning minified JSON into readable output — the same thing dedicated pretty-print tools do internally.
Why does JSON.stringify drop some of my object's properties?
JSON.stringify silently removes properties whose values are undefined, functions, or symbols — they vanish entirely from the output with no error. NaN and Infinity become null. Date objects become ISO strings. This silent dropping is a common source of bugs where a field disappears between client and server. If a field is missing, check whether its value was one of these unsupported types.
Why doesn't JSON.parse turn date strings back into Date objects?
JSON has no date type, so JSON.stringify converts Dates to ISO 8601 strings, and JSON.parse has no way to know a string was originally a Date. It stays a string. To revive them, pass a reviver function as the second argument to JSON.parse that detects the ISO date pattern and returns new Date(value), or convert the fields manually after parsing.
Can I use JSON.parse(JSON.stringify()) to deep copy an object?
Yes, for plain-data objects. JSON.parse(JSON.stringify(original)) creates a deep clone where nested objects and arrays are fully duplicated. But it loses functions, undefined values, and Dates-as-Dates, and throws on circular references. For objects with those types, use the built-in structuredClone() instead, which handles more data types correctly.
How do I safely parse JSON that might be malformed?
Wrap JSON.parse in a try/catch, because it throws a SyntaxError on invalid input like trailing commas, single quotes, or unquoted keys. Return a fallback value (like null or {}) in the catch block so one bad string doesn't crash your code. This matters most when parsing API responses or user input. Validate suspect JSON with the [free JSON tools](/tools) to find the exact error.
Try ToolsFuel
23+ free online tools for developers, designers, and everyone. No signup required.
Browse All Tools