What is the difference between JSON.stringify and JSON.parse?

Dark navy cover card with the title 'What is the difference between JSON.stringify and JSON.parse?' in white and a green accent bar

JSON.stringify and JSON.parse do opposite jobs. JSON.stringify takes a JavaScript value (an object, array, number and so on) and returns a JSON string. JSON.parse takes a JSON string and returns the JavaScript value it describes. That's the whole JSON.stringify vs JSON.parse difference: one goes out to text, the other comes back in from text.

JSON.stringify vs JSON.parse at a glance

JSON.stringify serializes and JSON.parse deserializes, and almost every other difference follows from that direction. Both are static methods on the built-in JSON object, which MDN describes as a namespace with methods for parsing and converting values to JSON. It isn't a constructor, so you never write new JSON().

JSON.stringify JSON.parse
Input Any JavaScript value A string of JSON text
Output A JSON string, or undefined Object, array, string, number, boolean or null
Optional arguments replacer, space reviver
Throws TypeError on circular references or BigInt SyntaxError on invalid JSON
Typical use Sending or storing data Reading data you received or loaded

If you're new to the format itself, my guide to what JSON format is covers the syntax both methods work with.

What JSON.stringify does

JSON.stringify converts a JavaScript value into JSON text. According to MDN's JSON.stringify reference, the full form is JSON.stringify(value, replacer, space), and both extra arguments are optional.

const user = { name: "Asha", admin: false, tags: ["a", "b"] };

JSON.stringify(user);
// '{"name":"Asha","admin":false,"tags":["a","b"]}'

JSON.stringify(user, null, 2);
// pretty-printed with two-space indentation

The two optional arguments change the output:

  • replacer is either a function that can change or drop each value, or an array of property names to keep.
  • space adds indentation for readability. A number is the count of spaces, clamped to 10. A string is cut to its first 10 characters.

Some values don't survive. undefined, functions and Symbols are dropped from objects and turned into null inside arrays. NaN and Infinity become null. Dates become strings, because Date has a toJSON() method that returns the same thing as toISOString(). A Map or Set becomes "{}", since only enumerable own properties are visited.

What JSON.parse does

JSON.parse reads a string of JSON text and builds the JavaScript value it describes. MDN's JSON.parse reference lists two forms: JSON.parse(text) and JSON.parse(text, reviver).

const text = '{"name":"Asha","admin":false}';
const user = JSON.parse(text);

user.admin; // false (a real boolean, not the text "false")

The parser is strict. Property names need double quotes and trailing commas are forbidden, so a valid JavaScript object literal can still fail with a SyntaxError. In Firefox the message list on MDN includes errors such as SyntaxError: JSON.parse: expected double-quoted property name. If you've hit that one, my post on why JSON requires double quotes explains the rule.

The optional reviver function runs on every parsed value, depth-first, starting with the most nested properties and finishing with the root under an empty-string key. Return undefined and that property is deleted, so a reviver that handles only some keys must pass every other value through unchanged.

How the two work together in a round trip

The most common pattern is stringify on the way out and parse on the way back in. MDN's example stores a session object in localStorage so it survives the browser closing:

localStorage.setItem("session", JSON.stringify(session));

// later, maybe after a reload
const restored = JSON.parse(localStorage.getItem("session"));

Plain data comes back intact, and MDN notes that JSON.parse(JSON.stringify(obj)) even keeps the original key order if the object is fully JSON-serializable. Anything else changes shape:

  • A Date comes back as an ISO string, not a Date. The JSON date format guide shows how to handle that.
  • Maps turn into {}. MDN's fix pairs a replacer that writes the entries out with a reviver that rebuilds the Map.
  • Very large integers can lose precision during parsing, before any reviver sees them. MDN suggests sending them as strings, and the reviver's context.source exposes the original text. More on that in JSON.parse and large numbers.

The double-stringify bug

The most confusing mistake with these two methods is calling JSON.stringify on something that is already a JSON string. Stringify sees a plain string, wraps it in quotes and escapes every inner quote:

const body = '{"id":7}';      // already JSON text
JSON.stringify(body);
// '"{\"id\":7}"'  <- a string containing escaped JSON

You spot it when an API stores or returns values like "{\"id\":7}" and one JSON.parse yields a string instead of an object. Typically a client builds the body with JSON.stringify(payload), then a helper wraps it again before sending. The server parses once and gets text.

The fix is to stringify exactly once, at the boundary where data leaves your code. For a value that's already double-encoded, parse it twice, or paste it into the JSON unescape tool.

When you don't need either method

You often don't need to call JSON.parse yourself. fetch gives you response.json(), which, per MDN's Response.json page, reads the body and resolves with the parsed JavaScript value. Despite the name, the result is an object, not JSON text. Don't parse it again.

Deep copying is the other case. JSON.parse(JSON.stringify(obj)) is a popular trick, but it inherits every loss listed above and throws on circular references. MDN recommends structuredClone(), which creates a deep clone and supports circular references.

My take: use the stringify-then-parse copy only for data that came from JSON in the first place, like an API response you want to mutate safely. If the object holds dates, Maps or class instances, use structuredClone().

Common errors and what they mean

Each method fails in its own way, and the error type tells you which side broke.

  • TypeError from JSON.stringify: the value has a circular reference, or it contains a BigInt. Remove the cycle or convert the BigInt with a replacer.
  • No error at all: JSON.stringify returns undefined when you pass undefined, a function or a Symbol at the top level. Nothing throws, so you find out when something downstream breaks.
  • SyntaxError from JSON.parse: the text isn't valid JSON. Typical causes are single quotes, trailing commas, an HTML error page instead of JSON, or an empty or truncated response. The post on Unexpected end of JSON input walks through the truncated case.

The limitation: neither method validates against a schema. JSON.parse checks that the text is valid JSON, not that id is a number or that a required field exists. Add your own checks or a schema validator before trusting the data.

FAQ

Is JSON.parse the opposite of JSON.stringify?

Mostly. JSON.stringify turns a value into JSON text and JSON.parse turns JSON text back into a value. The round trip is not perfect: dates come back as strings, Maps and Sets come back as empty objects, and undefined values and functions disappear.

Why does JSON.parse throw a SyntaxError?

JSON.parse throws a SyntaxError whenever the string is not valid JSON, for example single-quoted keys, a trailing comma, or a response that was cut off. Read the message and the position it reports, then fix the input or the code that produced it.

Do I need JSON.parse after fetch?

Not if you call response.json(). That method reads the body and resolves with the parsed JavaScript value, so running JSON.parse on its result fails or does nothing useful.

Is JSON.parse(JSON.stringify(obj)) a good way to deep copy?

It works only for plain data. It drops functions and undefined, turns dates into strings and throws on circular references. MDN points to structuredClone() for deep copies, which handles circular references.

Sources