Skip to main content
TF
By Rohit V.9 min readArticle

JSON.stringify vs JSON.parse in JavaScript

TF
ToolsFuel Team
Web development tools & tips
JavaScript code on a screen in a dark editor theme

Photo by Lautaro Andreani on Unsplash

The Two Directions, Defined

> Quick answer: `JSON.stringify(value)` converts a JavaScript value (object, array, number, etc.) into a JSON string — the form you send over the network or save to disk. `JSON.parse(string)` does the reverse, turning a JSON string back into a live JavaScript value. They're inverses: `JSON.parse(JSON.stringify(obj))` gets you a deep-ish copy of `obj`. The gotchas to remember: `stringify` silently drops `undefined`, functions, and symbols, and turns `Date` objects into strings (which don't auto-revive on parse). For readable output, pass `JSON.stringify(obj, null, 2)` to indent with 2 spaces. To inspect or format JSON by hand, the ToolsFuel JSON formatter does it instantly in the browser.

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

`JSON.stringify` takes a JavaScript value and returns its JSON text representation:

```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

Two developers reviewing code together at a workstation

Photo by John Schnobrich on Unsplash

`JSON.parse` takes a JSON string and returns the live JavaScript value:

```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

Both functions take extra arguments that solve real problems most developers handle the hard way.

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

Hands typing on a laptop keyboard with code on screen

Photo by Glenn Carstens-Peters on Unsplash

A few combinations come up constantly. Here are the ones worth committing to muscle memory.

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