{ }jsonkitOpen the tool

Working with JSON in JavaScript: parse, stringify, and Beyond

6 min read · updated 2026-07-12

JSON is the default data format of the web, and JavaScript ships with exactly two functions to move between JSON text and live objects: JSON.parse and JSON.stringify. They look trivial, but both accept optional callbacks most developers never touch, and both have sharp edges around dates, BigInt, circular references, and values that silently disappear.

This guide covers the full API surface: the reviver and replacer callbacks, the space argument for pretty-printing, toJSON(), why dates need special handling, the limits of the JSON round-trip for deep cloning, and how to parse untrusted input without crashing your app. Every section includes runnable code.

The two functions behind JSON in JavaScript

Working with JSON in JavaScript comes down to two built-in methods on the global JSON object. JSON.parse(text) reads a JSON string and returns the corresponding JavaScript value — object, array, string, number, boolean, or null. JSON.stringify(value) does the reverse, serializing a value into a JSON string. Neither method touches the network or the filesystem; they are pure, synchronous conversions between text and in-memory data.

The core round tripjs
const text = '{"name":"Ada","active":true,"score":42}';

const obj = JSON.parse(text);
console.log(obj.name);   // "Ada"
console.log(obj.score);  // 42 (a real number, not a string)

const backToText = JSON.stringify(obj);
console.log(backToText); // {"name":"Ada","active":true,"score":42}

That is 90% of everyday usage. The remaining 10% — the optional second and third arguments, and the edge cases around types JSON cannot represent — is where bugs hide. If you are new to the format itself, start with what is JSON and the JSON data types primer, then come back here.

JSON.parse and the reviver callback

JSON.parse accepts an optional second argument called the reviver: a function (key, value) => newValue invoked for every key/value pair, working from the innermost values outward. Return the value unchanged to keep it, return something new to transform it, or return undefined to drop the property. The reviver is the idiomatic place to rehydrate types JSON has no native syntax for.

Turning date strings back into Date objects

JSON has no date type, so dates are almost always serialized as ISO 8601 strings. When you parse them back, they are still strings. A reviver can detect and upgrade them:

Reviving ISO date stringsjs
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;

function dateReviver(key, value) {
  if (typeof value === "string" && ISO_DATE.test(value)) {
    return new Date(value);
  }
  return value;
}

const json = '{"title":"Launch","start":"2026-07-12T09:00:00.000Z"}';
const event = JSON.parse(json, dateReviver);

console.log(event.start instanceof Date); // true
console.log(event.start.getFullYear());   // 2026

The key argument is the property name (or the array index as a string); the top-level value is passed last with an empty-string key. Because the reviver walks bottom-up, nested objects are already transformed by the time their parent is visited.

JSON.stringify: space, replacer, and toJSON()

JSON.stringify(value, replacer, space) takes two optional arguments that control what gets serialized and how it is formatted.

The space argument for pretty-printing

Pass a number to indent with that many spaces (values above 10 are clamped to 10), or a string to indent with that literal string. This is exactly what a JSON formatter does under the hood.

Pretty-printing with indentationjs
const config = { host: "localhost", port: 8080, tls: false };

console.log(JSON.stringify(config, null, 2));
// {
//   "host": "localhost",
//   "port": 8080,
//   "tls": false
// }

console.log(JSON.stringify(config, null, "\t")); // tab-indented

The replacer: filtering and transforming

The replacer mirrors the reviver but runs during serialization. As a function (key, value) => newValue, it can omit or rewrite values; returning undefined drops the property. As an array of keys, it acts as an allowlist of properties to keep — useful for stripping sensitive fields.

Replacer as a function and as an allowlistjs
const user = { id: 1, name: "Ada", password: "secret", token: "abc" };

// Function form: redact sensitive keys
const safe = JSON.stringify(user, (key, value) =>
  key === "password" || key === "token" ? undefined : value
);
console.log(safe); // {"id":1,"name":"Ada"}

// Array form: keep only these keys
console.log(JSON.stringify(user, ["id", "name"]));
// {"id":1,"name":"Ada"}

toJSON(): let an object serialize itself

If a value has a toJSON() method, JSON.stringify calls it and serializes the returned value instead of the object. This is exactly why dates "just work" when stringifying — Date.prototype.toJSON returns an ISO string. You can add the same hook to your own classes:

Custom toJSON on a classjs
class Money {
  constructor(cents) { this.cents = cents; }
  toJSON() { return (this.cents / 100).toFixed(2); }
}

console.log(JSON.stringify({ price: new Money(1999) }));
// {"price":"19.99"}

What JSON.stringify silently drops or breaks

JSON.stringify cannot represent every JavaScript value, and its failure modes differ: some values vanish quietly, others throw. Knowing which is which prevents data-loss bugs that only surface in production.

Value typeWhat happens
undefinedDropped from objects; becomes null inside arrays
FunctionDropped from objects; becomes null inside arrays
SymbolDropped entirely
NaN, InfinitySerialized as null
BigIntThrows a TypeError
Circular referenceThrows a TypeError
DateBecomes an ISO string (via toJSON)
Map, SetBecomes {} — contents are lost
BigInt and circular references both throwjs
// BigInt -> TypeError: Do not know how to serialize a BigInt
try {
  JSON.stringify({ big: 9007199254740993n });
} catch (e) {
  console.log(e.name); // "TypeError"
}

// Circular reference -> TypeError
const node = { name: "a" };
node.self = node;
try {
  JSON.stringify(node);
} catch (e) {
  console.log(e.name); // "TypeError"
}

// A replacer can serialize BigInt as a string
const out = JSON.stringify({ big: 42n }, (key, value) =>
  typeof value === "bigint" ? value.toString() : value
);
console.log(out); // {"big":"42"}

The BigInt limitation matters when parsing, too: JSON numbers larger than Number.MAX_SAFE_INTEGER (2^53 − 1) lose precision when read by JSON.parse, because they are coerced to IEEE-754 doubles. If your API returns 64-bit IDs, transport them as strings.

Deep copies: structuredClone vs the JSON round-trip

For years the go-to trick for deep-cloning a plain object was JSON.parse(JSON.stringify(obj)). It works for JSON-safe data, but it inherits every limitation above: dates become strings, Map/Set become empty objects, undefined and functions disappear, and BigInt or circular references throw. Modern JavaScript has a purpose-built alternative, structuredClone(), available in all current browsers and in Node 17+.

Two ways to deep copy — and how they differjs
const original = {
  when: new Date("2026-01-01"),
  tags: new Set(["a", "b"]),
  note: undefined,
};

// JSON round-trip: lossy
const viaJson = JSON.parse(JSON.stringify(original));
console.log(viaJson.when instanceof Date); // false (it's a string)
console.log(viaJson.tags);                 // {} (Set is gone)
console.log("note" in viaJson);            // false (undefined dropped)

// structuredClone: type-preserving
const clone = structuredClone(original);
console.log(clone.when instanceof Date);   // true
console.log(clone.tags instanceof Set);    // true
console.log(clone.when === original.when); // false (a real copy)

Fetching and safely parsing JSON

Most JSON in a real app arrives over the network. The fetch API gives you a Response with a .json() method that reads the body and runs JSON.parse for you, returning a promise. Because it can reject — on a network failure or on a body that is not valid JSON — you should always guard it.

Fetching JSON with proper error handlingjs
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);

  if (!res.ok) {
    throw new Error(`Request failed: ${res.status}`);
  }

  // res.json() rejects if the body isn't valid JSON
  return res.json();
}

getUser(1)
  .then((user) => console.log(user.name))
  .catch((err) => console.error("Could not load user:", err));

When you call JSON.parse directly on a string — from localStorage, a query param, a message event, or a file — invalid input throws a SyntaxError. Never trust that a stored or incoming string is well-formed. A small wrapper keeps the failure local instead of crashing the caller:

A safe parse helper that never throwsjs
function safeParse(text, fallback = null) {
  try {
    return JSON.parse(text);
  } catch {
    return fallback;
  }
}

const prefs = safeParse(localStorage.getItem("prefs"), {});
console.log(prefs); // {} if the stored value was missing or corrupt
{ }JSON FormatterOpen the tool

When you are debugging a stringified payload by hand, a formatter makes the structure readable instantly — it is essentially JSON.stringify(value, null, 2) plus validation and syntax highlighting. Once your objects are flowing correctly, the JSON Minifier does the opposite for production payloads.

JSON ValidatorOpen the tool

Frequently asked questions

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

JSON.parse converts a JSON string into a live JavaScript value (object, array, number, etc.), while JSON.stringify does the reverse, serializing a value into a JSON string. Parse reads text into memory; stringify writes memory back out to text.

Why does JSON.stringify drop undefined and function values?

JSON has no representation for undefined, functions, or symbols, so JSON.stringify omits them from objects. Inside arrays, where a slot must exist, they are replaced with null instead. This is by design and follows the JSON spec.

How do I stringify an object that contains a BigInt?

JSON.stringify throws a TypeError on BigInt by default. Provide a replacer function that converts BigInts to strings, e.g. (k, v) => typeof v === 'bigint' ? v.toString() : v. On the way back, convert those strings to BigInt yourself in a reviver.

Should I use JSON.parse(JSON.stringify(obj)) to deep clone?

Only for plain JSON-safe data. It loses dates (they become strings), Map and Set, undefined, and functions, and it throws on circular references or BigInt. Prefer structuredClone(obj), which preserves these types and is built into modern browsers and Node 17+.

How do I convert a JSON date string back into a Date object?

Dates serialize to ISO strings but parse back as plain strings. Pass a reviver to JSON.parse that tests each string against an ISO date pattern and returns new Date(value) when it matches, leaving other values untouched.

How can I parse JSON without my app crashing on bad input?

Wrap JSON.parse in a try/catch, since malformed JSON throws a SyntaxError. A small safeParse(text, fallback) helper that returns a default value on failure keeps the error contained instead of propagating to the caller.