JSON.parse() vs JSON.stringify(): Complete Comparison
Every JavaScript developer uses JSON.parse() and JSON.stringify(), but many treat them as simple mirror images of each other without understanding their distinct behaviors, parameters, and failure modes. While they are indeed complementary — one serializes, the other deserializes — they each have unique capabilities and pitfalls that can produce subtle bugs when misunderstood.
This guide provides a complete comparison of both methods. We will cover their signatures, parameters, return values, edge cases, and real-world patterns. By the end, you will know exactly when to reach for each method and how to avoid the traps that catch even experienced developers.
What Do JSON.parse() and JSON.stringify() Do?
At the highest level, these two methods handle the conversion between JavaScript values and JSON text:
JSON.stringify(value)takes a JavaScript value (object, array, string, number, boolean, or null) and returns a JSON-formatted string.JSON.parse(text)takes a JSON-formatted string and returns the corresponding JavaScript value.
Together, they enable serialization (converting in-memory data to a text format for storage or transmission) and deserialization (reconstructing in-memory data from text). This is the foundation of virtually all JSON-based communication in web applications: API calls, localStorage, message queues, configuration files, and more.
// Serialization: object -> string
const user = { name: "Alice", age: 30, active: true };
const jsonString = JSON.stringify(user);
// '{"name":"Alice","age":30,"active":true}'
// Deserialization: string -> object
const parsed = JSON.parse(jsonString);
// { name: "Alice", age: 30, active: true }
console.log(typeof jsonString); // "string"
console.log(typeof parsed); // "object"Method Signatures Compared
JSON.stringify()
JSON.stringify(value, replacer?, space?)value— The value to serialize.replacer— Optional. An array of property names to include, or a function that transforms values during serialization.space— Optional. A number (0-10) or string for indentation. Controls pretty-printing.
JSON.parse()
JSON.parse(text, reviver?)text— The JSON string to parse.reviver— Optional. A function that transforms values during deserialization. Called for every key-value pair, from the innermost values outward.
Notice the asymmetry: JSON.stringify() has three parameters (value, replacer, space) while JSON.parse() has two (text, reviver). There is no equivalent to the space parameter on the parsing side because formatting is irrelevant when converting text to an object — whitespace is discarded.
Replacer vs. Reviver: Transforming Data During Conversion
Both methods accept a callback function that transforms values during conversion. These callbacks look similar but serve opposite purposes and have different call patterns.
The Replacer (stringify)
The replacer is called during serialization. It receives each key-value pair and returns the value that should appear in the JSON output. Returning undefined removes the property:
const data = {
username: "alice",
password: "s3cret!",
email: "alice@example.com",
lastLogin: new Date("2025-12-01T10:00:00Z")
};
const safe = JSON.stringify(data, (key, value) => {
if (key === 'password') return undefined; // remove
if (key === 'email') return '***redacted***'; // mask
return value; // keep
}, 2);
console.log(safe);Output:
{
"username": "alice",
"email": "***redacted***",
"lastLogin": "2025-12-01T10:00:00.000Z"
}The Reviver (parse)
The reviver is called during deserialization. It receives each key-value pair after parsing and returns the value that should be stored in the resulting object. This is commonly used to reconstruct types that JSON does not natively support, like Date objects:
const jsonString = '{"name":"Alice","createdAt":"2025-12-01T10:00:00.000Z","score":95.5}';
const result = JSON.parse(jsonString, (key, value) => {
// Convert ISO date strings back to Date objects
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
console.log(result.createdAt instanceof Date); // true
console.log(result.createdAt.getFullYear()); // 2025Without the reviver, result.createdAt would be a plain string. The reviver lets you restore type information that JSON cannot represent natively.
What Each Method Cannot Handle
Both methods have limitations, but they fail in different ways.
JSON.stringify() Silently Drops Values
JSON.stringify() does not throw errors for most unsupported types. Instead, it silently omits them or converts them to null:
const obj = {
fn: () => "hello", // function — dropped
sym: Symbol("id"), // Symbol — dropped
undef: undefined, // undefined — dropped
nan: NaN, // NaN — becomes null
inf: Infinity, // Infinity — becomes null
date: new Date(), // Date — becomes ISO string (via toJSON)
regex: /abc/gi, // RegExp — becomes empty object {}
map: new Map([["a", 1]]) // Map — becomes empty object {}
};
console.log(JSON.stringify(obj, null, 2));Output:
{
"nan": null,
"inf": null,
"date": "2025-12-01T10:00:00.000Z",
"regex": {},
"map": {}
}Five properties survived. Three were dropped entirely (fn, sym, undef). Two were converted to null. And RegExp and Map were turned into empty objects, which is arguably worse than being dropped because it looks like valid data.
JSON.parse() Throws on Invalid Input
Unlike JSON.stringify() which silently handles problematic values, JSON.parse() is strict. If the input is not valid JSON, it throws a SyntaxError:
// All of these throw SyntaxError:
JSON.parse("undefined"); // undefined is not valid JSON
JSON.parse("{'name': 'Alice'}"); // single quotes not allowed
JSON.parse("{name: 'Alice'}"); // unquoted keys not allowed
JSON.parse('{"a": 1,}'); // trailing commas not allowed
JSON.parse(''); // empty string not valid
// These are valid:
JSON.parse('null'); // returns null
JSON.parse('"hello"'); // returns "hello"
JSON.parse('42'); // returns 42
JSON.parse('true'); // returns trueThe strictness of JSON.parse() is actually helpful — it forces you to deal with malformed input immediately rather than letting bad data silently propagate through your application. Always wrap JSON.parse() in a try-catch when parsing user input or external data.
The Round-Trip Problem
A natural expectation is that stringifying and then parsing (or vice versa) should give you back the original value. This is true for simple objects but breaks down for many real-world data types:
const original = {
date: new Date("2025-06-15"),
pattern: /^[a-z]+$/i,
items: new Set([1, 2, 3]),
nothing: undefined,
count: NaN
};
const roundTripped = JSON.parse(JSON.stringify(original));
console.log(typeof roundTripped.date); // "string" (not Date)
console.log(roundTripped.pattern); // {} (not RegExp)
console.log(roundTripped.items); // {} (not Set)
console.log(roundTripped.nothing); // undefined (key missing)
console.log(roundTripped.count); // null (not NaN)After the round trip, the Date became a string, the RegExp and Set became empty objects, undefined was dropped, and NaN became null. This is why JSON.parse(JSON.stringify(obj)) is not a reliable deep clone method. For cloning, use structuredClone() instead.
If you need to preserve types across serialization, use the replacer and reviver together:
// Custom type-preserving serialization
function serialize(obj) {
return JSON.stringify(obj, (key, value) => {
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
if (value instanceof Set) {
return { __type: 'Set', value: [...value] };
}
if (value instanceof RegExp) {
return { __type: 'RegExp', source: value.source, flags: value.flags };
}
return value;
}, 2);
}
function deserialize(json) {
return JSON.parse(json, (key, value) => {
if (value && value.__type === 'Date') return new Date(value.value);
if (value && value.__type === 'Set') return new Set(value.value);
if (value && value.__type === 'RegExp') return new RegExp(value.source, value.flags);
return value;
});
}
const data = {
created: new Date("2025-06-15"),
tags: new Set(["json", "tutorial"]),
pattern: /^\d+$/g
};
const restored = deserialize(serialize(data));
console.log(restored.created instanceof Date); // true
console.log(restored.tags instanceof Set); // true
console.log(restored.pattern instanceof RegExp); // trueCommon Mistakes
1. Parsing What Is Already an Object
If you receive data from a library or framework that has already parsed the JSON for you, calling JSON.parse() on it will throw an error or produce unexpected results:
// fetch() already parses the response
const response = await fetch('/api/users');
const data = await response.json(); // Already an object!
// WRONG: data is already an object, not a string
const users = JSON.parse(data); // TypeError or unexpected result
// CORRECT: use data directly
console.log(data.users);2. Double-Stringifying
The reverse mistake is equally common: stringifying a value that is already a string. The result is a JSON string containing an escaped string — technically valid, but not what you intended:
const jsonStr = '{"name":"Alice"}';
// WRONG: jsonStr is already a string
const doubled = JSON.stringify(jsonStr);
console.log(doubled); // '"{\"name\":\"Alice\"}"'
// The receiver now has to parse TWICE to get the object
JSON.parse(JSON.parse(doubled)); // { name: "Alice" }3. Not Handling Parse Errors
JSON.parse() throws on invalid input, and unhandled exceptions crash your application. Always wrap it in a try-catch when parsing external data:
function safeParse(jsonString, fallback = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('Failed to parse JSON:', error.message);
return fallback;
}
}
// Safe even with bad input
const config = safeParse(userInput, { theme: "light" });4. Ignoring the Reviver for Dates
One of the most common bugs in JavaScript applications is comparing dates that came from a JSON.parse() call. Because JSON has no date type, dates become strings after a round trip. Code like if (user.createdAt > someDate) silently does a string comparison instead of a date comparison. Use a reviver to convert date strings back to Date objects.
Best Practices
- Always validate before parsing. If you are parsing JSON from user input, an API, or a file, validate it first. Use a JSON Validator to catch syntax errors before they become runtime exceptions.
- Use try-catch around JSON.parse(). Never assume that incoming JSON strings are valid. Network errors, encoding issues, and upstream bugs can all produce malformed JSON.
- Use the replacer to protect sensitive data. Before logging or transmitting objects, strip passwords, tokens, and personal information using a replacer function.
- Use the reviver to restore types. If your data contains dates, regular expressions, or custom types, use the reviver callback in
JSON.parse()to reconstruct them properly. - Prefer structuredClone() over JSON round-trips for cloning. The
JSON.parse(JSON.stringify())pattern loses data.structuredClone()handles Date, RegExp, Map, Set, ArrayBuffer, and more. - Keep serialization and deserialization symmetric. If you use a custom replacer in
JSON.stringify(), write a matching reviver forJSON.parse()to ensure your data survives the round trip without type loss.
Quick Reference: When to Use Which
Here is a practical guide for choosing between the two methods:
Use JSON.stringify() when you need to:
- Send data to an API (request body)
- Save data to
localStorageorsessionStorage - Log structured data to the console or a file
- Create a hash or fingerprint of an object
- Serialize state for transmission between web workers or iframes
Use JSON.parse() when you need to:
- Process an API response received as a string
- Read data from
localStorageorsessionStorage - Parse a JSON configuration file loaded from disk
- Convert a JSON string from a WebSocket message into an object
- Deserialize data received from a message queue or event stream
Try It Now
Want to see serialization and deserialization in action? Our JSON Stringify tool lets you convert JavaScript objects to JSON strings instantly, and our JSON Unstringify tool reverses the process — turning escaped JSON strings back into readable objects.
If you are debugging a JSON payload and suspect it might have syntax errors, run it through the JSON Validator to get precise error messages pointing to the exact location of the problem. All tools run entirely in your browser — no data is sent to any server.
Conclusion
JSON.parse() and JSON.stringify() are two sides of the same coin, but they are not perfect mirrors. JSON.stringify() silently drops unsupported values and provides three parameters for controlling output. JSON.parse() is strict about its input and provides a reviver for restoring types. Understanding these asymmetries is the key to using both methods effectively.
The biggest takeaway is that JSON is a text format, not a type system. It supports strings, numbers, booleans, null, objects, and arrays — nothing else. Every other JavaScript type must be explicitly converted before serialization and explicitly restored after deserialization. When you respect this limitation and write your replacers and revivers accordingly, JSON.parse() and JSON.stringify() become a reliable and powerful pair for data interchange.
The next time you find yourself debugging a mysterious undefined value or a date comparison that always fails, check your serialization boundary. Chances are, the answer lies in how your data survived — or did not survive — the journey through JSON.