LEARNING OBJECTIVES โต
- Understand why naive
JSON.stringify()fails on advanced JavaScript data types (Date,Map,Set,BigInt,RegExp). - Implement custom
JSON.stringifyreplacer functions to encode rich data types. - Implement custom
JSON.parsereviver functions to restore typed object prototypes. - Defend against fatal
TypeError: Converting circular structure to JSONcrashes using weak reference tracking.
๐ The Mental Model & Story (Intuitive Foundation)
Imagine sending a complex Lego sculpture of a pirate ship across the country in a flat envelope. You cannot put the 3D ship into the slot as-is. You must carefully disassemble the ship into flat bricks, record an assembly instruction manual with colored tags, and slide the flat pieces into the envelope.
When your friend receives the envelope, if they just dump the bricks on the floor without reading the instructions, they just have a pile of plastic rectanglesโnot a ship. To get the ship back, they must read the tagged instruction manual and reassemble every mast, cannon, and deck back into its original 3D form.
+---------------------------------------------------------------------------------------------------+
| THE SERIALIZATION LIFECYCLE (Disassembly & Reassembly) |
| |
| [ Rich JS Object Graph ] |
| - Date: new Date() |
| - Map: new Map([['key', 42]]) |
| - Set: new Set(['admin', 'user']) |
| | |
| v JSON.stringify(payload, customReplacer) |
| [ Flat UTF-16 DOMString in localStorage ] |
| '{"date":{"__type":"Date","val":"2026-08-21T00:00:00Z"},"roles":{"__type":"Set","val":["admin"]}}'|
| | |
| v JSON.parse(rawString, customReviver) |
| [ Restored JS Instances with Active Methods (.getTime(), .has(), .get()) ] |
+---------------------------------------------------------------------------------------------------+
In Web Storage, the storage engine only accepts flat UTF-16 strings. Serializing is disassembling the ship; deserializing with a reviver is rebuilding it with all its original methods intact.
Technical Deep Dive & Specifications
The Limitations of Standard JSON.stringify()
Default JSON.stringify() only supports primitive numbers, strings, booleans, arrays, null, and plain object literals. All other native JavaScript constructs suffer silent data corruption or throw runtime errors:
// 1. Dates lose their class prototype and become ISO strings:
const d = new Date();
JSON.stringify(d); // ""2026-08-21T02:00:00.000Z"" -> Becomes a plain string!
// d.getFullYear() works; JSON.parse(JSON.stringify(d)).getFullYear() throws TypeError!
// 2. Maps serialize to empty objects:
const m = new Map([['status', 'active']]);
JSON.stringify(m); // "{}" -> All Map entries lost!
// 3. Sets serialize to empty objects:
const s = new Set([1, 2, 3]);
JSON.stringify(s); // "{}" -> All Set items lost!
// 4. BigInt throws a fatal TypeError:
const b = 900719925474099100n;
JSON.stringify(b); // Uncaught TypeError: Do not know how to serialize a BigInt
// 5. Undefined & Functions are completely omitted:
const obj = { fn: () => {}, val: undefined, visible: 1 };
JSON.stringify(obj); // '{"visible":1}'
// 6. Circular references throw an uncaught exception:
const node = {};
node.self = node;
JSON.stringify(node); // Uncaught TypeError: Converting circular structure to JSON
JSON Data Type Handling Matrix
| Data Structure | Default JSON.stringify |
Result of Default JSON.parse |
Data Loss / Error | Custom Replacer/Reviver Solution |
|---|---|---|---|---|
Date |
"2026-08-21T..." |
String (not Date) |
โ ๏ธ Methods lost (.getTime()) |
Wrap with __type: 'Date' |
Map |
{} |
{} |
โ Total loss of keys & values | Wrap with __type: 'Map', entries array |
Set |
{} |
{} |
โ Total loss of values | Wrap with __type: 'Set', elements array |
BigInt |
๐ฅ TypeError |
N/A (Crashes) | โ Application Crash | Wrap with __type: 'BigInt', stringified digits |
RegExp |
{} |
{} |
โ Total loss of pattern & flags | Wrap with __type: 'RegExp', source & flags |
Circular Ref |
๐ฅ TypeError |
N/A (Crashes) | โ Application Crash | Track visited nodes with WeakSet |
Architecting Custom Replacers and Revivers
The second argument of JSON.stringify(value, replacer) and JSON.parse(text, reviver) allows intercepting every key-value pair during the traversal of the object graph.
JSON.stringify(val, replacer) JSON.parse(str, reviver)
| |
v v
+------------------------------------------+ +------------------------------------------+
| Check Type: | | Check if value is tagged object: |
| If Date -> { __t: 'Date', v: ISO } | | If __t === 'Date' -> return new Date(v)|
| If Map -> { __t: 'Map', v: [...m] } | | If __t === 'Map' -> return new Map(v) |
| If Set -> { __t: 'Set', v: [...s] } | | If __t === 'Set' -> return new Set(v) |
| If BigInt -> { __t: 'BigInt', v: str } | | If __t === 'BigInt' -> return BigInt(v) |
| Else -> return default value | | Else -> return value |
+------------------------------------------+ +------------------------------------------+
Defending Against Circular References
When an object references itself directly or indirectly, JSON.stringify recurses infinitely until it throws a TypeError. We can protect storage writes using a WeakSet to track visited object references.
๐ป Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 50โ69 (
superReplacer): Detects rich non-standard instances (BigInt,Map,Set,RegExp) and transforms them into standard object dictionaries containing explicit__dataTypemetadata tags and array representations. - Lines 72โ92 (
superReviver): Intercepts parsed objects containing the__dataTypetag and instantiates actual ES6 class objects (new Map(),new Set(),BigInt(),new Date()). - Lines 95โ105 (
safeStringify): Uses an in-flightWeakSetto detect previously visited object references, converting circular cycles into safe sentinel strings ("[Circular Reference]") instead of throwing fatal runtime errors. - Lines 135โ144: Validates that all class methods (
.getFullYear(),.get(),.has(),.test()) work natively on the restored object without manual conversions.
Expected Browser Render Output
+--------------------------------------------------------------------------+
| Complex Data Serialization Engine |
| [ Serialize & Store Rich Object ] [ Deserialize & Revive Types ] |
| |
| 1. Stored Raw JSON in localStorage: |
| { |
| "title": "Enterprise Task State", |
| "createdAt": "2026-08-21T02:25:00.000Z", |
| "metadata": { |
| "__dataType": "Map", |
| "value": [["assignedTo", "Devin"], ["priority", "P0-Critical"]] |
| }, |
| "tags": { |
| "__dataType": "Set", |
| "value": ["frontend", "security", "storage"] |
| }, |
| "bigIdentifier": { "__dataType": "BigInt", "value": "900719925..." }, |
| "selfReference": "[Circular Reference]" |
| } |
| |
| 2. Revitalized Object Inspection & Method Verification: |
| [ok] Date check: instanceof Date = true | .getFullYear(): 2026 |
| [ok] Map check: instanceof Map = true | .get('priority'): P0-Critical |
| [ok] Set check: instanceof Set = true | .has('security'): true |
| [ok] BigInt check: typeof === 'bigint' = true |
+--------------------------------------------------------------------------+๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Build a Complete Typed Storage Engine
Construct a reusable TypedStorage class with set(key, value) and get(key) methods that transparently supports storing and revitalizing arrays, objects, Date objects, Map, and Set without requiring the caller to manually parse or transform anything.
Your Goal:
- Implement
TypedStorage.set(key, value): Automatically serialize complex types with type metadata. - Implement
TypedStorage.get(key): Automatically revitalize objects with exact class prototypes. - Defend against corrupted or non-JSON string values previously stored in the storage slot by falling back cleanly to returning the raw string.
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Invoking
JSON.stringify(bigint)Directly:BigIntdoes not have a.toJSON()method. Attempting to stringify aBigIntthrows an immediate uncaughtTypeErrorthat will crash your application. - Assuming Date Prototypes Persist:
JSON.parse(JSON.stringify(new Date()))returns a string primitive, not aDateinstance. Calling.getTime()on it will throw aTypeError. - Unsanitized Object Keys (Prototype Pollution): When reviving untrusted JSON payloads containing keys like
__proto__orconstructor, ensure your parser does not assign them directly to Object prototypes.
๐ก Pro Tips
- Use
.toJSON()on Custom Classes: You can define a.toJSON()method on any custom class or domain model.JSON.stringify()will automatically call your.toJSON()method before serialization. - Compression for Large Payloads: For payloads approaching 100KBโ500KB, evaluate compression libraries like
lz-string(LZString.compressToUTF16(str)) before writing tolocalStorageto save up to 70% of quota space.
๐ Key Takeaways
JSON.stringify()natively fails to preserveDate,Map,Set,BigInt, andRegExp.JSON.stringify(val, replacer)enables custom encoding for complex types.JSON.parse(str, reviver)enables dynamic revitalization of typed instances.- Circular references can be defended against using
WeakSetreference tracking during serialization. - Always protect
JSON.parse()calls withtry...catchto prevent unhandled syntax errors from corrupt strings. - --