← Back to Blog
·10 min read

JSON.parse and JSON.stringify Have Landmines — Here's Every One of Them

Every JavaScript developer uses JSON.parse and JSON.stringify constantly and understands them about 60% of the way. The remaining 40% is a collection of edge cases that have caused production bugs in every codebase that has lived long enough. This is the complete reference.

JSON.stringify silently drops undefined values

This is the most common surprise. JSON.stringify silently omits object properties with undefined values. It also converts array elements that are undefined to null. And it drops properties whose values are functions or Symbols entirely.

const obj = {
  name: 'Alice',
  age: undefined,         // DROPPED — not serialized
  role: null,             // kept — null is valid JSON
  greet: () => 'hello',  // DROPPED — functions not serializable
  [Symbol('id')]: 42,    // DROPPED — Symbols not serializable
};

JSON.stringify(obj);
// '{"name":"Alice","role":null}'
// 'age', 'greet', and Symbol key are silently gone

const arr = [1, undefined, 3];
JSON.stringify(arr);
// '[1,null,3]'  — undefined becomes null in arrays

// If you need to preserve undefined:
JSON.stringify(obj, (key, value) =>
  value === undefined ? '__undefined__' : value
);

Date objects serialize to strings that don't deserialize back

JSON.stringify converts Date objects to ISO 8601 strings. JSON.parse does not convert them back — they remain strings. This is a one-way transformation that has bitten countless developers who serialize a Date, store it, parse it back, and discover they now have a string where they expected a Date.

const event = {
  name: 'Conference',
  date: new Date('2026-05-27'),
};

const json = JSON.stringify(event);
// '{"name":"Conference","date":"2026-05-27T00:00:00.000Z"}'

const parsed = JSON.parse(json);
parsed.date instanceof Date; // false — it's a string
typeof parsed.date;          // 'string'

// Fix: use a reviver function
const reparsed = JSON.parse(json, (key, value) => {
  // ISO date pattern: "2026-05-27T00:00:00.000Z"
  if (typeof value === 'string' && /^d{4}-d{2}-d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;
});

reparsed.date instanceof Date; // true

BigInt throws — and there is no built-in fix

JSON.stringify throws a TypeError when it encounters a BigInt value. There is no built-in fallback because JSON has no BigInt type — JSON numbers are IEEE 754 doubles, which cannot represent integers larger than 2^53 - 1 accurately. This matters for database IDs (Snowflake IDs, Twitter IDs), financial values, and any integer from a system that uses 64-bit integers.

const id = 9007199254740993n; // larger than Number.MAX_SAFE_INTEGER

JSON.stringify({ id }); // TypeError: Do not know how to serialize a BigInt

// Fix 1: convert to string (most common approach)
JSON.stringify({ id }, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"id":"9007199254740993"}'

// Fix 2: use a library like 'json-bigint' that preserves numeric type
import JSONbig from 'json-bigint';
JSONbig.stringify({ id }); // '{"id":9007199254740993}'
const parsed = JSONbig.parse('{"id":9007199254740993}');
typeof parsed.id; // 'bigint'

The space parameter most developers have never used

JSON.stringify accepts a third parameter: space. Pass a number for indentation spaces, or a string to use as the indent character. The canonical usage is JSON.stringify(obj, null, 2) for human-readable output. The second parameter is the replacer (covered next).

const data = { name: 'Alice', roles: ['admin', 'editor'] };

// Compact (default)
JSON.stringify(data);
// '{"name":"Alice","roles":["admin","editor"]}'

// Indented with 2 spaces
JSON.stringify(data, null, 2);
// {
//   "name": "Alice",
//   "roles": [
//     "admin",
//     "editor"
//   ]
// }

// Indented with tabs
JSON.stringify(data, null, '	');

Replacer and reviver: the powerful parameters nobody uses

Both JSON.stringify and JSON.parse accept a function parameter that intercepts every value during processing. The replacer (for stringify) and reviver (for parse) give you full control over the transformation:

// Replacer: filter to specific fields only (allowlist)
const user = { id: 1, name: 'Alice', password: 'secret', email: 'a@b.com' };
const allowedFields = ['id', 'name', 'email'];

JSON.stringify(user, allowedFields);
// '{"id":1,"name":"Alice","email":"a@b.com"}'
// 'password' is excluded

// Replacer: transform values during serialization
JSON.stringify(user, (key, value) => {
  if (key === 'password') return undefined; // exclude
  if (value instanceof Date) return value.toISOString();
  if (typeof value === 'bigint') return value.toString();
  return value;
});

// Reviver: transform values during parsing
const json = '{"createdAt":"2026-05-27T10:00:00Z","score":"9007199254740993"}';
const parsed = JSON.parse(json, (key, value) => {
  if (key === 'createdAt') return new Date(value);
  if (key === 'score') return BigInt(value);
  return value;
});

The toJSON method: how objects control their own serialization

If an object has a toJSON() method,JSON.stringify will call it and serialize the return value instead of the object itself. This is how Date works — it has a toJSON() method that returns its ISO string. You can use this to define serialization behavior for any class:

class Money {
  constructor(public amount: number, public currency: string) {}

  toJSON() {
    return { amount: this.amount, currency: this.currency };
  }
}

class User {
  constructor(public name: string, public balance: Money) {}

  toJSON() {
    // Exclude sensitive fields, transform nested objects
    return { name: this.name, balance: this.balance };
  }
}

const user = new User('Alice', new Money(42.50, 'USD'));
JSON.stringify(user);
// '{"name":"Alice","balance":{"amount":42.5,"currency":"USD"}}'

NaN and Infinity: the silent nulls

JSON.stringify convertsNaN and Infinity (both positive and negative) to null. This is per-spec — JSON has no representation for these values. The conversion is silent: no warning, no error. The receiver gets null and has no way to know whether the original value was null or was NaN or Infinity.

JSON.stringify({ a: NaN, b: Infinity, c: -Infinity });
// '{"a":null,"b":null,"c":null}'

// Guard against this in financial/scientific code:
function safeValue(n: number): number | null {
  if (!isFinite(n)) {
    console.warn('Non-finite value encountered:', n);
    return null; // or throw, depending on your tolerance
  }
  return n;
}

const ratio = dividend / divisor; // might be Infinity if divisor is 0
const payload = { ratio: safeValue(ratio) };

The pattern to adopt: any computation that might produce NaN or Infinity should be guarded before the result enters a JSON payload. Do not rely on JSON.stringify to handle it gracefully — it will give you a null in the output and no indication that information was lost.

Published May 27, 2026 · By the utili.dev Team