JSON.parse() vs JSON.stringify()

Complete guide to converting between JSON strings and JavaScript objects

JSONJune 22, 202611 min readBy Keyur Patel

Every JavaScript developer uses JSON.parse() and JSON.stringify() regularly — they are the two most fundamental methods for working with JSON data. Yet many developers use them without fully understanding their nuances, optional parameters, edge cases, and performance implications. This guide covers everything you need to know about both methods, from basic usage to advanced techniques that senior developers rely on daily.

Quick Comparison

FeatureJSON.parse()JSON.stringify()
DirectionString → ObjectObject → String
InputValid JSON stringJavaScript value
OutputJavaScript object/array/valueJSON string
Error on invalid inputThrows SyntaxErrorThrows TypeError (circular ref)
Optional 2nd paramReviver functionReplacer function or array
Optional 3rd paramSpace (indentation)
Common useReceiving API dataSending API data

JSON.parse() — String to Object

JSON.parse() takes a valid JSON string and converts it into a JavaScript value — an object, array, string, number, boolean, or null. This is how you read data from APIs, localStorage, configuration files, and any external source that provides JSON as text.

Basic Syntax

const jsonString = '{"name": "Alice", "age": 30, "isAdmin": true}'
const user = JSON.parse(jsonString)

console.log(user.name)    // → "Alice"
console.log(user.age)     // → 30
console.log(user.isAdmin) // → true
console.log(typeof user)  // → "object"

// Parsing arrays
const arr = JSON.parse('[1, 2, 3, "hello"]')
console.log(arr[0])  // → 1
console.log(arr[3])  // → "hello"

// Parsing primitives
JSON.parse('"hello"')  // → "hello" (string)
JSON.parse('42')       // → 42 (number)
JSON.parse('true')     // → true (boolean)
JSON.parse('null')     // → null

The Reviver Function (2nd Parameter)

The reviver is a function called for each key-value pair during parsing. It lets you transform values as they are parsed — the most common use case is converting date strings back into Date objects:

// Problem: JSON has no Date type — dates become strings
const json = '{"name": "Alice", "createdAt": "2024-05-01T12:00:00.000Z"}'
const parsed = JSON.parse(json)
console.log(typeof parsed.createdAt) // → "string" (not a Date!)

// Solution: Use a reviver to convert date strings
const withDates = JSON.parse(json, (key, value) => {
  // Check if value looks like an ISO date string
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    return new Date(value)
  }
  return value
})
console.log(withDates.createdAt instanceof Date) // → true
console.log(withDates.createdAt.getFullYear())   // → 2024

// Reviver for filtering/transforming values
const filtered = JSON.parse('{"a": 1, "b": 2, "c": 3}', (key, value) => {
  if (key === "b") return undefined // Remove this property
  if (typeof value === "number") return value * 10 // Multiply numbers
  return value
})
// → { a: 10, c: 30 } — "b" was removed, numbers were multiplied

Error Handling

JSON.parse() throws a SyntaxError when the input is not valid JSON. Always wrap it in try/catch when parsing external data:

// ❌ Dangerous — crashes if input is invalid
const data = JSON.parse(userInput)

// ✅ Safe — handles errors gracefully
function safeParse(jsonString, fallback = null) {
  try {
    return JSON.parse(jsonString)
  } catch (error) {
    console.error("Invalid JSON:", error.message)
    return fallback
  }
}

const data = safeParse(userInput, {})
// Returns {} instead of crashing if input is invalid

// Common causes of 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 comma not allowed

JSON.stringify() — Object to String

JSON.stringify() converts a JavaScript value into a JSON string. This is how you send data to APIs, save to localStorage, write to files, or serialize objects for transmission.

Basic Syntax

const user = { name: "Alice", age: 30, isAdmin: true }
const jsonString = JSON.stringify(user)

console.log(jsonString)
// → '{"name":"Alice","age":30,"isAdmin":true}'
console.log(typeof jsonString) // → "string"

// Stringify arrays
JSON.stringify([1, 2, 3])  // → '[1,2,3]'

// Stringify primitives
JSON.stringify("hello")  // → '"hello"' (note: wrapped in quotes)
JSON.stringify(42)       // → '42'
JSON.stringify(true)     // → 'true'
JSON.stringify(null)     // → 'null'

Pretty Printing (3rd Parameter)

The third parameter controls indentation — pass a number for spaces or a string for custom indent characters:

const user = { name: "Alice", roles: ["admin", "editor"], settings: { theme: "dark" } }

// Compact (default) — for network transmission
JSON.stringify(user)
// → '{"name":"Alice","roles":["admin","editor"],"settings":{"theme":"dark"}}'

// Pretty-printed with 2 spaces — for readability
JSON.stringify(user, null, 2)
// → {
//     "name": "Alice",
//     "roles": [
//       "admin",
//       "editor"
//     ],
//     "settings": {
//       "theme": "dark"
//     }
//   }

// Tab indentation
JSON.stringify(user, null, "\t")

The Replacer Function (2nd Parameter)

The replacer controls which properties appear in the output. It can be a function or an array:

const user = { name: "Alice", password: "secret123", email: "alice@example.com", age: 30 }

// Replacer as array — only include specified keys
JSON.stringify(user, ["name", "email"])
// → '{"name":"Alice","email":"alice@example.com"}'

// Replacer as function — transform or filter values
JSON.stringify(user, (key, value) => {
  if (key === "password") return undefined  // Remove sensitive fields
  if (typeof value === "string") return value.toUpperCase()
  return value
})
// → '{"name":"ALICE","email":"ALICE@EXAMPLE.COM","age":30}'

// Real-world: Remove null/undefined values
const clean = JSON.stringify(data, (key, value) => {
  if (value === null || value === undefined) return undefined
  return value
})

What JSON.stringify() Omits

Not everything can be serialized to JSON. These values are silently dropped or converted:

const obj = {
  name: "Alice",
  age: undefined,        // ❌ OMITTED — undefined disappears
  greet: function() {},  // ❌ OMITTED — functions disappear
  id: Symbol("id"),      // ❌ OMITTED — Symbols disappear
  score: NaN,            // ⚠️ Becomes null
  value: Infinity,       // ⚠️ Becomes null
  date: new Date(),      // ⚠️ Becomes ISO string (not reversible)
  regex: /test/gi,       // ⚠️ Becomes {} (empty object)
  map: new Map(),        // ⚠️ Becomes {} (loses data)
  set: new Set([1,2,3]), // ⚠️ Becomes {} (loses data)
}

JSON.stringify(obj)
// → '{"name":"Alice","score":null,"value":null,"date":"2024-05-01T...","regex":{},"map":{},"set":{}}'
// Note: age, greet, and id are completely gone!

// In arrays, omitted values become null:
JSON.stringify([1, undefined, function(){}, 4])
// → '[1,null,null,4]'

Real-World Use Cases

API Communication (fetch)

// Sending data to an API (stringify)
const response = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Alice", email: "alice@example.com" })
})

// Receiving data from an API (parse)
const data = await response.json() // Internally calls JSON.parse()
console.log(data.id) // → 123

localStorage / sessionStorage

// Storage only accepts strings — must stringify objects
const settings = { theme: "dark", fontSize: 14, language: "en" }
localStorage.setItem("settings", JSON.stringify(settings))

// Reading back — must parse the string
const stored = localStorage.getItem("settings")
const parsed = JSON.parse(stored)
console.log(parsed.theme) // → "dark"

// Safe pattern with fallback
function getStoredSettings() {
  try {
    return JSON.parse(localStorage.getItem("settings")) || {}
  } catch {
    return {}
  }
}

Deep Cloning Objects

// Quick deep clone using parse + stringify
const original = { user: { name: "Alice", scores: [10, 20, 30] } }
const clone = JSON.parse(JSON.stringify(original))

clone.user.name = "Bob"
clone.user.scores.push(40)

console.log(original.user.name)   // → "Alice" (unchanged)
console.log(original.user.scores) // → [10, 20, 30] (unchanged)

// ⚠️ Limitations of this approach:
// - Loses undefined, functions, Symbols
// - Converts Dates to strings
// - Fails on circular references
// - Slower than structuredClone()

// ✅ Modern alternative (2022+):
const clone2 = structuredClone(original)
// Handles Dates, Maps, Sets, circular refs — but not functions

Comparing Objects

// Objects can't be compared with ===
{ a: 1 } === { a: 1 } // → false (different references)

// Quick equality check using stringify
function shallowEqual(a, b) {
  return JSON.stringify(a) === JSON.stringify(b)
}

shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }) // → true
shallowEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) // → false ⚠️ (key order matters!)

// For order-independent comparison, sort keys first:
function deepEqual(a, b) {
  return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))
}

Common Mistakes Developers Make

⚠️ Double-stringifying data

Calling JSON.stringify() on a value that is already a string produces a double-quoted string. Check if data is already a string before stringifying.

JSON.stringify("hello") // → '"hello"' (wrapped in extra quotes!)

⚠️ Double-parsing data

Calling JSON.parse() on a value that is already an object throws an error or produces unexpected results.

JSON.parse({name: 'Alice'}) // ❌ TypeError — input must be a string

⚠️ Not handling parse errors

JSON.parse() throws on invalid input. Without try/catch, your entire application can crash from malformed API responses or corrupted localStorage.

// Always: try { JSON.parse(data) } catch { /* handle */ }

⚠️ Assuming key order is preserved

JSON spec does not guarantee object key order. While modern engines preserve insertion order, comparing stringified objects for equality is fragile.

JSON.stringify({b:2, a:1}) !== JSON.stringify({a:1, b:2})

⚠️ Losing Date objects

Dates become ISO strings after stringify. When you parse them back, they remain strings unless you use a reviver.

JSON.parse(JSON.stringify({d: new Date()})).d // → string, not Date

⚠️ Ignoring circular references

Objects that reference themselves (e.g., DOM nodes, parent-child relationships) cause JSON.stringify() to throw a TypeError.

const a = {}; a.self = a; JSON.stringify(a) // ❌ TypeError

Best Practices

Always wrap JSON.parse() in try/catch

Never assume external JSON is valid. Network errors, API changes, and corrupted data are common.

Use the replacer to remove sensitive data

Before logging or transmitting objects, strip passwords, tokens, and PII with a replacer function.

Use structuredClone() for deep cloning

The parse/stringify trick works but has limitations. structuredClone() is faster and handles more types.

Validate parsed data with schemas

After parsing, validate the structure matches what you expect using Zod, Yup, or JSON Schema.

Use reviver for Date reconstruction

Always implement a date reviver when working with APIs that return ISO date strings.

Consider performance for large objects

JSON.stringify() on very large objects (10MB+) can block the main thread. Consider Web Workers for heavy serialization.

Custom Serialization with toJSON()

If an object has a toJSON() method, JSON.stringify() will call it and use the return value instead of the object itself. This gives you complete control over serialization:

class User {
  constructor(name, email, password) {
    this.name = name
    this.email = email
    this.password = password
    this.createdAt = new Date()
  }

  // Custom serialization — exclude password, format date
  toJSON() {
    return {
      name: this.name,
      email: this.email,
      createdAt: this.createdAt.toISOString(),
      // password is intentionally excluded
    }
  }
}

const user = new User("Alice", "alice@example.com", "secret123")
JSON.stringify(user, null, 2)
// → { "name": "Alice", "email": "alice@example.com", "createdAt": "2024-05-01T..." }
// Password is never serialized!

Interview Questions

These JSON-related questions commonly appear in JavaScript interviews:

Q: What is the output of JSON.stringify(undefined)?

A: undefined (not the string 'undefined' — the function returns undefined, not a string, because undefined is not a valid JSON value).

Q: How do you deep clone an object without structuredClone?

A: JSON.parse(JSON.stringify(obj)) — but note it loses functions, undefined, Symbols, Dates (become strings), and fails on circular references.

Q: What does JSON.parse(JSON.stringify(new Date())) return?

A: A string (ISO format), not a Date object. JSON has no Date type — dates are serialized as strings and must be manually reconstructed.

Q: How do you handle circular references in JSON.stringify()?

A: Use a replacer function with a WeakSet to track seen objects, or use libraries like flatted or circular-json.

Q: What is the difference between JSON.stringify({a: undefined}) and JSON.stringify({a: null})?

A: The first produces '{}' (undefined properties are omitted). The second produces '{"a":null}' (null is a valid JSON value).

Performance Considerations

JSON.parse() and JSON.stringify() are synchronous and run on the main thread. For most applications, they are fast enough. But for very large payloads, they can cause jank:

  • Under 1MB: No concerns — both methods are instant.
  • 1-10MB: May cause a brief pause (10-50ms). Usually acceptable.
  • 10MB+: Can block the main thread for 100ms+. Consider using a Web Worker.
  • For streaming large JSON: Use libraries like stream-json in Node.js or the Response.json() method in browsers (which is optimized internally).

Pro tip: response.json() in the Fetch API is generally faster than manually calling JSON.parse(await response.text())because browsers can optimize the internal parsing pipeline.

Handling Types That JSON Does Not Support

JSON only supports six data types: string, number, boolean, null, object, and array. JavaScript has many more types that need special handling when serializing and deserializing. Understanding how each type behaves prevents data loss in your applications.

Dates

Dates are the most common problem. JSON.stringify() converts Date objects to ISO 8601 strings automatically (because Date has a built-in toJSON method). But JSON.parse() does not convert them back — you get plain strings. This means a round-trip through JSON silently changes your Date objects into strings, which can cause subtle bugs when you later try to call Date methods on them.

The solution is to always use a reviver function that detects ISO date patterns and reconstructs Date objects during parsing. Alternatively, store dates as Unix timestamps (numbers) which survive the round-trip without any special handling, though you lose human readability.

Maps and Sets

Map and Set are modern JavaScript collection types that JSON.stringify() cannot handle correctly. Both serialize to empty objects because JSON has no concept of Map or Set. If you need to serialize these types, convert them to arrays first: use Array.from(map) for Maps (produces an array of key-value pairs) and Array.from(set) for Sets (produces an array of values). During parsing, reconstruct them with new Map(array) and new Set(array).

BigInt

BigInt values cause JSON.stringify() to throw a TypeError — it does not silently convert or omit them like it does with undefined. This is because there is no safe way to represent arbitrary-precision integers in JSON (regular JSON numbers are IEEE 754 doubles, which lose precision beyond 2^53). The workaround is to convert BigInts to strings before stringifying, and parse them back with a reviver that detects numeric strings that need BigInt conversion.

Circular References

Circular references occur when an object references itself directly or indirectly — for example, a parent object that has a children array where each child points back to the parent. DOM nodes are the most common source of circular references in web development. JSON.stringify() throws a TypeError on circular structures. Solutions include using a replacer function that tracks seen objects with a WeakSet and skips duplicates, using the flatted library which preserves circular references as special tokens, or restructuring your data to avoid cycles.

Functions and Classes

Functions, class instances, and Symbol values are completely invisible to JSON serialization. Properties with function values are silently omitted. Class instances lose their prototype chain — they become plain objects with only own enumerable properties. If you need to serialize class instances, implement a toJSON() method that returns a plain object representation, and create a static fromJSON() method to reconstruct the instance during parsing.

JSON.parse and JSON.stringify in Node.js vs Browser

While JSON.parse() and JSON.stringify() have the same API in both environments, there are practical differences in how and when you use them:

Browser Context

In the browser, you most commonly use these methods with the Fetch API (serializing request bodies, parsing responses), localStorage/sessionStorage (which only stores strings), Web Workers (passing data between threads via structured cloning or JSON), and WebSocket messages (which are transmitted as text). The browser's response.json() method is optimized and should be preferred over manually calling JSON.parse(await response.text()).

Node.js Context

In Node.js, additional use cases include reading and writing JSON configuration files (package.json, tsconfig.json), parsing request bodies in Express/Fastify middleware, streaming JSON from databases and APIs, and inter-process communication. Node.js also provides require() which can directly import JSON files without manual parsing, and the fs module for reading JSON files from disk.

For large JSON files in Node.js, consider stream-based parsing with libraries like JSONStream or stream-json, which process data incrementally without loading the entire file into memory. This is essential when processing multi-gigabyte JSON log files or database exports.

Security Considerations

In older JavaScript environments, developers used eval() to parse JSON strings — this is extremely dangerous because eval executes arbitrary code. JSON.parse() is safe because it only parses data structures and never executes code. However, you should still validate the parsed output before trusting it. An attacker cannot inject code through JSON.parse(), but they can provide unexpected data shapes that break your application logic if not validated. Always validate parsed API responses against expected schemas before using the data.

Frequently Asked Questions

What is the difference between JSON.parse() and JSON.stringify()?
JSON.parse() converts a JSON string into a JavaScript object. JSON.stringify() does the opposite — it converts a JavaScript object into a JSON string. They are inverse operations.
What happens if JSON.parse() receives invalid JSON?
JSON.parse() throws a SyntaxError exception. Always wrap it in a try/catch block to handle malformed input gracefully.
Can JSON.stringify() handle circular references?
No. JSON.stringify() throws a TypeError when it encounters circular references. You need to use a replacer function or a library like flatted to handle circular structures.
Does JSON.stringify() preserve undefined values?
No. Properties with undefined values are omitted from the output. In arrays, undefined is converted to null. Functions and Symbols are also omitted.
How do I pretty-print JSON in JavaScript?
Use JSON.stringify(obj, null, 2) where the third argument (2) specifies the number of spaces for indentation.
Is JSON.parse() safe to use with user input?
JSON.parse() itself is safe — it only parses data, it does not execute code. However, you should validate the parsed data structure before trusting it in your application logic.
What is the reviver function in JSON.parse()?
The reviver is an optional second argument to JSON.parse() that lets you transform values during parsing. It is commonly used to convert date strings back into Date objects.
What is the replacer function in JSON.stringify()?
The replacer is an optional second argument to JSON.stringify() that controls which properties are included in the output. It can be a function that transforms values or an array of property names to include.

Related JSON Tools

Summary

JSON.parse() and JSON.stringify() are inverse operations — one converts strings to objects, the other converts objects to strings. Master their optional parameters (reviver, replacer, space) to handle dates, filter sensitive data, and produce readable output. Always handle parse errors with try/catch, be aware of what stringify omits (undefined, functions, Symbols), and consider performance for large payloads. These two methods are the foundation of all JSON data exchange in JavaScript applications.