From circular references to BigInt, Dates, and custom replacers — how to serialize JavaScript objects safely and predictably.

Introduction
At first glance, JSON.stringify
looks simple:
JSON.stringify({ a: 1 });
// '{"a":1}'
But real-world objects are messy: nested structures, Dates, Maps, Sets, circular references, undefined
, NaN
, BigInt
, and sensitive fields you want stripped.
If you just call JSON.stringify
blindly, you’ll hit surprises:
JSON.stringify({ x: undefined }) // {}
JSON.stringify({ date: new Date() }) // {"date":"2025-09-07T11:52:59.348Z"}
JSON.stringify({ big: 10n }) // ❌ TypeError: Do not know how to serialize a BigInt
This post is a practical guide to JSON.stringify
for complex data—with tips, real-world examples, and patterns you’ll actually use in apps, APIs, and logs.
1. The Basics Refresher
JSON.stringify(obj, replacer, space);
obj
→ the value to stringify.replacer
→ function/array to control what gets included.space
→ pretty-printing (number of spaces or a string).
const data = { a: 1, b: 2 };
JSON.stringify(data, null, 2);
/*
{
"a": 1,
"b": 2
}
*/
2. Handling undefined
, NaN
, and Functions
JSON.stringify({ a: undefined, b: NaN, c: () => {} });
// '{"b":null}'
undefined
and functions → removed (if property value).- Inside arrays → replaced with
null
. NaN
andInfinity
→null
.
👉 If you need them preserved, you’ll need a replacer.
3. Circular References (Crash Warning)
const obj = {};
obj.self = obj;
JSON.stringify(obj);
// ❌ TypeError: Converting circular structure to JSON
Fix: Use a replacer or library
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;
});
}
const obj = { name: "Ali" };
obj.self = obj;
console.log(safeStringify(obj));
// {"name":"Ali","self":"[Circular]"}
4. Dates
By default, Date
objects are serialized as ISO strings:
JSON.stringify({ now: new Date("2025-09-07") });
// {"now":"2025-09-07T00:00:00.000Z"}
If you need timestamps instead:
const obj = {
now: new Date("2025-09-07"),
toJSON() { return this.now.getTime(); }
};
JSON.stringify(obj);
// {"now":1757203200000}
👉 Use .toJSON
override for custom date formats.
5. BigInt
BigInts are not JSON-safe.
JSON.stringify({ big: 10n });
// ❌ TypeError
Workaround: Convert to string
const obj = { big: 10n };
const json = JSON.stringify(obj, (_, v) =>
typeof v === "bigint" ? v.toString() : v
);
// {"big":"10"}
Or use libraries like json-bigint
if you need safe round-tripping.
6. Maps and Sets
By default, they serialize to {}
:
JSON.stringify({ m: new Map([["a", 1]]) });
// {"m":{}}
Convert manually:
const obj = {
m: new Map([["a", 1]]),
s: new Set([1, 2, 3])
};
const json = JSON.stringify(obj, (_, v) => {
if (v instanceof Map) return Object.fromEntries(v);
if (v instanceof Set) return [...v];
return v;
});
console.log(json);
// {"m":{"a":1},"s":[1,2,3]}
7. Filtering Sensitive Data
Use the replacer function to remove secrets:
const user = { id: 1, name: "Ali", password: "secret" };
const json = JSON.stringify(user, (key, value) =>
key === "password" ? undefined : value
);
console.log(json); // {"id":1,"name":"Ali"}
👉 Great for logging or sending safe API responses.
8. Pretty Printing and Debug Logs
const obj = { id: 1, data: { x: 10, y: 20 } };
console.log(JSON.stringify(obj, null, 2));
Output:
{
"id": 1,
"data": {
"x": 10,
"y": 20
}
}
Use spaces (2
, 4
) for readability in logs.
9. Deterministic Key Order
Objects don’t guarantee property order before ES2015, but modern JS preserves insertion order (with numeric keys sorted first).
If you need stable order for hashing or diffs:
function stableStringify(obj) {
return JSON.stringify(obj, Object.keys(obj).sort());
}
👉 Ensures consistent JSON output across runs.
10. TypeScript Tips
A) Strong typing for replacers
function stringifySafe<T>(value: T): string {
return JSON.stringify(value, (_, v) => {
if (typeof v === "bigint") return v.toString();
return v;
});
}
B) Exclude undefined fields in types
type User = { id: number; name?: string };
const u: User = { id: 1 };
console.log(JSON.stringify(u)); // {"id":1}
11. Alternatives and Tools
structuredClone
→ deep copy, not serialization, but handles circular refs.
2. Libraries:
flatted
→ circular-safe stringify/parse.superjson
→ handles Dates, Maps, Sets, BigInt automatically.json-stable-stringify
→ stable key ordering.
12. Handy Utility Recipes
Safe stringify with circular + BigInt
function safeJSON(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, val) => {
if (typeof val === "bigint") return val.toString();
if (typeof val === "object" && val !== null) {
if (seen.has(val)) return "[Circular]";
seen.add(val);
}
return val;
}, 2);
}
Redact sensitive keys
const REDACT = ["password", "token"];
const json = JSON.stringify(obj, (k, v) =>
REDACT.includes(k) ? "***" : v
);
Conclusion
JSON.stringify
is more than “just stringify an object.”
With the right patterns, you can:
- ✅ Handle Dates, BigInts, Maps/Sets, and circular refs.
- ✅ Strip sensitive fields before logging.
- ✅ Make logs pretty and stable across runs.
- ✅ Keep apps safe with redaction and validation.
Pro Tip: Treat JSON.stringify
as a serialization contract—decide what must be included, in what shape, and how to handle tricky values. Wrap it in utility functions so your team has consistent, bug-free behavior.
Call to Action (CTA)
What’s the weirdest JSON.stringify
bug you’ve hit—circular refs, BigInt crashes, or missing fields?
Drop it in the comments, and share this guide with a teammate who still thinks JSON.stringify
is “just simple.”
Leave a Reply