JSON.stringify Tips for Complex Data

Posted by

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

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 and Infinitynull.

👉 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

  1. structuredClone → deep copy, not serialization, but handles circular refs.

2. Libraries:


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

Your email address will not be published. Required fields are marked *