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; // trueBigInt 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.
Try it yourself
JSON Formatter — validate and pretty-print JSON instantly →