Skip to main content

Json.parse and json.stringify in JavaScript

JSON.stringify() converts a JavaScript value to a JSON string. JSON.parse() converts a JSON string back to a JavaScript value.

Theory

TL;DR

  • stringify packs an object into a portable text string; parse unpacks it back into a working object.
  • stringify drops functions, undefined, and Symbols silently; Dates become ISO strings; BigInt throws a TypeError.
  • parse validates strictly and throws SyntaxError on any malformed input.
  • Use both for: API request bodies, localStorage, config files on disk.
  • Skip both for: Dates you need as Date objects, functions, or circular references.

Quick example

javascript
const user = { name: 'Alice', age: 30, active: true }; const json = JSON.stringify(user); // '{"name":"Alice","age":30,"active":true}' const parsed = JSON.parse(json); console.log(parsed.name); // 'Alice' console.log(typeof json); // 'string' console.log(typeof parsed); // 'object'

stringify gives you a plain string with no methods. parse gives back an object you can actually use.

Key difference

JSON.stringify() builds a string by recursively walking the value, calling toJSON() on objects that have it, and silently skipping anything without a JSON equivalent. JSON.parse() does the reverse but validates every byte against RFC 8259. Single quotes, unquoted keys, trailing commas - all of these throw. The round-trip is not lossless: what stringify drops cannot come back through parse.

When to use

  • API requests: JSON.stringify(body) before sending, response.json() after receiving.
  • localStorage: setItem('key', JSON.stringify(data)) to save, JSON.parse(getItem('key')) to load.
  • Debug output: JSON.stringify(obj, null, 2) for a readable, indented snapshot in logs.
  • Node.js config: JSON.parse(fs.readFileSync('config.json', 'utf8')) on app start.
  • Skip for: Dates, functions, classes, circular references, BigInt.

What stringify changes or drops

javascript
const obj = { fn: () => 'hello', // dropped from object undef: undefined, // dropped from object sym: Symbol('id'), // dropped from object date: new Date('2023-01-01'), // becomes ISO string nan: NaN, // becomes null }; JSON.stringify(obj); // '{"date":"2023-01-01T00:00:00.000Z","nan":null}' // In arrays, undefined and functions become null: JSON.stringify([1, undefined, () => {}, 3]); // '[1,null,null,3]' // BigInt throws immediately - no silent conversion: JSON.stringify({ id: 123n }); // TypeError: Do not know how to serialize a BigInt

replacer and reviver

Both methods have an optional parameter for custom handling. The replacer in stringify filters or transforms values on the way out. The reviver in parse restores types on the way in.

javascript
// replacer as array: property whitelist JSON.stringify({ name: 'Bob', password: 'secret' }, ['name']); // '{"name":"Bob"}' // replacer as function: handle BigInt JSON.stringify({ id: 123n }, (key, value) => typeof value === 'bigint' ? value.toString() : value ); // '{"id":"123"}' // reviver: restore Date objects after parse const json = '{"name":"Alice","birthday":"1990-06-15T00:00:00.000Z"}'; const result = JSON.parse(json, (key, value) => key === 'birthday' ? new Date(value) : value ); console.log(result.birthday instanceof Date); // true

The replacer is called top-down from the root; the reviver is called bottom-up, nested properties first.

How V8 handles this internally

In V8, JSON.stringify() runs through a C++ serializer that walks the object graph, writes primitives directly, and calls toJSON() when present. JSON.parse() is a recursive descent parser that validates UTF-8 and checks syntax per RFC 8259. Both run in native code. That is why they outperform any JavaScript-based JSON library by a wide margin.

Common mistakes

1. Forgetting to parse after reading from localStorage

javascript
localStorage.setItem('user', JSON.stringify({ name: 'Alice' })); // Wrong const raw = localStorage.getItem('user'); console.log(raw.name); // undefined - raw is a string, not an object // Fix const user = JSON.parse(localStorage.getItem('user')); console.log(user.name); // 'Alice'

In production apps this is probably the most common JSON bug. The storage layer always returns strings.

2. Expecting Date to survive the round-trip

javascript
const obj = { birthday: new Date('1990-01-01') }; const clone = JSON.parse(JSON.stringify(obj)); console.log(clone.birthday instanceof Date); // false console.log(clone.birthday); // '1990-01-01T00:00:00.000Z' // clone.birthday.getFullYear() → TypeError // Fix: use a reviver const restored = JSON.parse(JSON.stringify(obj), (k, v) => k === 'birthday' ? new Date(v) : v ); console.log(restored.birthday instanceof Date); // true

3. Circular references crash stringify

javascript
const obj = { x: 1 }; obj.self = obj; JSON.stringify(obj); // TypeError: Converting circular structure to JSON // Fix: replacer with WeakSet function safeStringify(value) { const seen = new WeakSet(); return JSON.stringify(value, (key, val) => { if (typeof val === 'object' && val !== null) { if (seen.has(val)) return '[Circular]'; seen.add(val); } return val; }); }

4. Passing a non-string to JSON.parse()

javascript
JSON.parse({ a: 1 }); // SyntaxError // The object coerces to "[object Object]", which is not valid JSON. // Always pass an actual JSON string: JSON.parse('{"a":1}'); // { a: 1 }

5. Using JSON for deep clone when you have special types

javascript
const original = { date: new Date(), map: new Map([[1, 'one']]) }; const broken = JSON.parse(JSON.stringify(original)); // broken.date is a string, broken.map is {} // Use structuredClone() instead: const proper = structuredClone(original); console.log(proper.date instanceof Date); // true console.log(proper.map instanceof Map); // true

Real-world usage

  • React: persist state to localStorage via stringify on change, parse on mount (common in Next.js for theme preferences).
  • Express: express.json() middleware auto-parses incoming JSON bodies; res.json() auto-stringifies the response.
  • Node.js: fs.writeFileSync('config.json', JSON.stringify(config, null, 2)) for human-readable config files.
  • Fetch API: body: JSON.stringify(data) on POST requests, await response.json() on the response.
  • Redux Persist: serializes store slices to localStorage between sessions.

Follow-up questions

Q: What happens to undefined in an object vs. in an array?
A: In an object, the key is dropped entirely: JSON.stringify({a: undefined}) gives '{}'. In an array, undefined becomes null to keep index positions: JSON.stringify([undefined]) gives '[null]'.

Q: replacer accepts both a function and an array. When do you choose the array?
A: Use an array for a quick property whitelist: JSON.stringify(user, ['name', 'email']). Use a function when you need conditional logic or type transformation across nested properties.

Q: What is the difference between JSON.parse() and eval()?
A: eval() executes arbitrary JavaScript, which is dangerous with untrusted data. JSON.parse() only accepts valid JSON and cannot run code. Always use JSON.parse() for data from external sources.

Q: How does JSON.parse() handle duplicate keys in a JSON string?
A: The last value wins. JSON.parse('{"a":1,"a":2}') returns {a: 2}. The spec permits duplicate keys, but producing them is always a bug in the source.

Q: (Senior) Implement a stringify that handles circular references without any libraries.
A: Track visited objects in a WeakSet. In the replacer function, check if the current value is already in the set before adding it. WeakSet is the right choice because it holds references weakly and does not block garbage collection.

Examples

localStorage: save and restore settings

javascript
const settings = { theme: 'dark', fontSize: 16, notifications: true }; // Save localStorage.setItem('settings', JSON.stringify(settings)); // Restore (guard against null - key might not exist yet) const raw = localStorage.getItem('settings'); const loaded = raw !== null ? JSON.parse(raw) : settings; console.log(loaded.theme); // 'dark'

Always check for null before calling JSON.parse(). localStorage.getItem() returns null for missing keys, and accessing properties on it later will throw.

POST request with Fetch

javascript
async function createUser(data) { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) // object → string for the wire }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); // parses the JSON response body } createUser({ name: 'Alice', role: 'developer' }) .then(user => console.log('Created with id:', user.id));

response.json() is essentially JSON.parse(await response.text()). It throws if the response body is not valid JSON, so check response.ok first.

replacer and reviver for edge cases

javascript
// Stringify: drop sensitive fields, handle BigInt const payload = { userId: 9007199254740993n, // too large for a safe JS number name: 'Bob', password: 'secret' }; const serialized = JSON.stringify(payload, (key, value) => { if (key === 'password') return undefined; // drop it if (typeof value === 'bigint') return value.toString(); // convert return value; }); // '{"userId":"9007199254740993","name":"Bob"}' // Parse: restore Date from API response const apiResponse = '{"id":1,"createdAt":"2023-06-15T10:00:00.000Z"}'; const record = JSON.parse(apiResponse, (key, value) => key === 'createdAt' ? new Date(value) : value ); console.log(record.createdAt instanceof Date); // true console.log(record.createdAt.getFullYear()); // 2023

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?