JSON.stringify vs JSON.parse: Complete Guide with Examples
Every JavaScript developer uses JSON.stringify() and JSON.parse() daily - but most only scratch the surface. This guide covers the full API, real-world patterns, and the surprising edge cases that catch developers off guard.
The Core Problem: JavaScript Objects Are Not JSON
JavaScript objects live in memory as structured data with types, prototypes, and references. JSON is a text format - a flat string with a strict grammar. The moment you need to send data across a network, store it in localStorage, or log it to a file, you need to cross the boundary between the two. That is exactly what these two functions do:
JSON.stringify(value)- Serializes a JavaScript value into a JSON string (object → text)JSON.parse(text)- Deserializes a JSON string back into a JavaScript value (text → object)
Getting this wrong causes silent data loss, runtime errors, and security vulnerabilities. Understanding both functions deeply will save you from hours of debugging.
JSON.stringify() in Depth
The full signature is JSON.stringify(value, replacer, space). Most developers only use the first argument, but all three are powerful.
Basic Usage
const user = { id: 1, name: "Alice", role: "admin" };
// Compact (for network transmission)
JSON.stringify(user);
// '{"id":1,"name":"Alice","role":"admin"}'
// Pretty-printed (for logging or human-readable files)
JSON.stringify(user, null, 2);
// {
// "id": 1,
// "name": "Alice",
// "role": "admin"
// }
// Tab-indented
JSON.stringify(user, null, "\t");
The Replacer Parameter: Filter and Transform Output
The second argument to JSON.stringify() is the replacer. It can be an array (allowlist of keys) or a function (transform each value):
// Array replacer - only include these keys
JSON.stringify(user, ["id", "name"]);
// '{"id":1,"name":"Alice"}' - role is excluded
// Function replacer - transform values
JSON.stringify(user, (key, value) => {
if (key === "role") return undefined; // omit this key
if (typeof value === "string") return value.toUpperCase();
return value;
});
// '{"id":1,"name":"ALICE"}'
The function replacer is called with every key-value pair, including nested ones. Returning undefined from the replacer omits that key entirely from the output.
Custom toJSON() Method
Any object can control its own serialization by implementing a toJSON() method. JSON.stringify() calls this automatically if it exists:
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
toJSON() {
return `${this.amount} ${this.currency}`;
}
}
JSON.stringify({ price: new Money(9.99, "USD") });
// '{"price":"9.99 USD"}'
JSON.parse() in Depth
The full signature is JSON.parse(text, reviver). The reviver is the counterpart to the replacer - it transforms parsed values back into rich JavaScript objects.
Basic Usage
const json = '{"id":1,"name":"Alice","createdAt":"2026-03-26T10:00:00Z"}';
const parsed = JSON.parse(json);
// { id: 1, name: 'Alice', createdAt: '2026-03-26T10:00:00Z' }
// Note: createdAt is a STRING, not a Date object
The Reviver Function: Restore Types
JSON has no native Date type - dates become strings. The reviver function lets you restore them on the way in:
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
const parsed = JSON.parse(json, (key, value) => {
if (typeof value === "string" && ISO_DATE.test(value)) {
return new Date(value);
}
return value;
});
parsed.createdAt instanceof Date; // true
parsed.createdAt.getFullYear(); // 2026
Real-World Patterns
Pattern 1: Sending Data to an API
// Serialize before sending
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice", email: "alice@example.com" })
});
// Parse the response
const data = await response.json(); // response.json() calls JSON.parse() internally
Pattern 2: LocalStorage (Strings Only)
// Save state to localStorage
localStorage.setItem("cart", JSON.stringify(cartItems));
// Restore state from localStorage
const saved = localStorage.getItem("cart");
const cartItems = saved ? JSON.parse(saved) : [];
Pattern 3: Deep Clone (With Caveats)
// Quick deep clone for plain objects - no references, no functions
const original = { a: 1, nested: { b: 2 } };
const clone = JSON.parse(JSON.stringify(original));
// Mutating clone does NOT affect original
clone.nested.b = 99;
original.nested.b; // still 2
The
JSON.parse(JSON.stringify(obj))deep clone only works for plain data. It dropsundefined, functions, Symbols, and non-serializable types. For production cloning, usestructuredClone()(available in modern browsers and Node 17+) instead.
Format & Validate Your JSON Instantly
Paste your JSON to format it with 2-space indentation, syntax-highlight it, and catch any parse errors before they hit your app. Free, runs entirely in your browser.
Open JSON Formatter →Step-by-Step: Debugging a stringify / parse Problem
- Check what you are stringifying. Run
console.log(typeof value)before calling stringify. If it is already a string, you will double-encode it. - Check for circular references. If you get
TypeError: Converting circular structure to JSON, you have an object that references itself. - Check for lost data. If fields disappear after stringify + parse, they probably contained
undefined, a function, or a Symbol. - Wrap parse in try/catch. Never trust external JSON strings. Always catch the SyntaxError from malformed input.
- Inspect with a formatter. Use our JSON Formatter to pretty-print and visually verify the structure before and after.
Common Pitfalls and How to Fix Them
Pitfall 1: undefined, Functions, and Symbols Are Silently Dropped
const obj = {
name: "Alice",
greet: function() { return "hi"; }, // dropped
id: undefined, // dropped
sym: Symbol("x") // dropped
};
JSON.stringify(obj);
// '{"name":"Alice"}' - greet, id, and sym are gone with no error
Fix: if you need to serialize functions or undefined values, use a replacer function to convert them to a string representation first.
Pitfall 2: Date Objects Become Strings
const obj = { ts: new Date("2026-03-26") };
const roundTripped = JSON.parse(JSON.stringify(obj));
roundTripped.ts instanceof Date; // false - it is a string now
typeof roundTripped.ts; // "string"
Fix: use a reviver function in JSON.parse() to restore Date objects, as shown above.
Pitfall 3: Circular References Throw
const a = {};
a.self = a; // circular reference
JSON.stringify(a);
// TypeError: Converting circular structure to JSON
Fix: use a replacer that tracks seen objects, or use a library like flatted that supports circular JSON.
Pitfall 4: BigInt Throws
JSON.stringify({ id: 9007199254740993n });
// TypeError: Do not know how to serialize a BigInt
Fix: use a replacer to convert BigInt to string: (key, val) => typeof val === 'bigint' ? val.toString() : val
Pitfall 5: Double Parsing
// API returns a JSON string that contains JSON (already-serialized)
const raw = '"{\\"name\\":\\"Alice\\"}"'; // a JSON-encoded JSON string
JSON.parse(raw); // returns the string '{"name":"Alice"}'
// You need to parse it TWICE
JSON.parse(JSON.parse(raw)); // returns { name: 'Alice' }
This happens when backends double-serialize data. Always inspect the type of what JSON.parse() returns - if it is still a string, parse again.
Performance Considerations
For most applications, JSON serialization is fast enough to ignore. But for high-throughput APIs or large datasets, keep these in mind:
- Minify in production: Do not pass a
spaceargument in API responses. The whitespace adds bytes for no machine benefit. - Avoid stringify in hot loops: Serializing the same large object thousands of times per second is expensive. Cache the stringified output if the object is immutable.
- Consider alternatives for extreme performance: Libraries like
fast-json-stringify(uses a schema to generate optimized code) can be 2-5x faster than native for known shapes. - Stream large payloads: For multi-megabyte JSON, streaming parsers like
JSONStreamorclarinetavoid loading the entire document into memory.
Quick Reference: What Gets Lost in stringify?
undefinedvalues - key is dropped from objects, becomesnullin arrays- Functions - dropped silently
- Symbol keys and Symbol values - dropped silently
- Date objects - converted to ISO string (not restored on parse)
- BigInt - throws TypeError
- Circular references - throws TypeError
- RegExp objects - become
{} - Map, Set, WeakMap - become
{} - NaN, Infinity, -Infinity - become
null
Frequently Asked Questions
Can JSON.parse throw an error?
Yes. JSON.parse() throws a SyntaxError if the input is not valid JSON. Always wrap it in a try/catch when parsing untrusted or external data. A safe wrapper: function safeParse(s, fallback = null) { try { return JSON.parse(s); } catch { return fallback; } }
Is JSON.stringify(JSON.parse(str)) the same as the original string?
Not necessarily. JSON.stringify(JSON.parse(str)) produces valid, normalized JSON, but key order may differ, extra whitespace is removed, and numbers may be reformatted. It is good for normalization but not for exact round-trip fidelity.
What is the difference between JSON.stringify and toString()?
toString() calls the object's prototype method, which for plain objects returns [object Object] - useless. JSON.stringify() produces a valid JSON string. Always use JSON.stringify() when you need structured text output.
Why does JSON.parse return a string sometimes?
If the input JSON is a quoted string (e.g., '"hello"'), JSON.parse() correctly returns the JavaScript string "hello". This is valid behavior - a JSON document can be any JSON value, not just an object or array.
How do I handle NaN and Infinity in JSON?
JSON does not support NaN or Infinity. JSON.stringify() converts them to null silently. If you need these values, use a replacer to convert them to strings like "NaN" or "Infinity", and a reviver to restore them on parse.
Use our free tool to format, validate, and inspect any JSON string instantly → Open JSON Formatter.
Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.