๐Ÿ’พ Chapter 48: Web Storage API

Storing Complex Data & Serialization

Custom JSON replacers and revivers: Preserving `Date`, `Map`, `Set`, `BigInt`, and defending against circular references.

LEARNING OBJECTIVES โŒต
  • Understand why naive JSON.stringify() fails on advanced JavaScript data types (Date, Map, Set, BigInt, RegExp).
  • Implement custom JSON.stringify replacer functions to encode rich data types.
  • Implement custom JSON.parse reviver functions to restore typed object prototypes.
  • Defend against fatal TypeError: Converting circular structure to JSON crashes using weak reference tracking.
๐ŸŽฌ INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
๐ŸŒ
1. Input
Directives & Tags
โš™๏ธ
2. Parse
Tokenizer & AST
๐ŸŒณ
3. Layout
Box Model & Flow
๐ŸŽจ
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

๐Ÿ“– 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 __dataType metadata tags and array representations.
  • Lines 72โ€“92 (superReviver): Intercepts parsed objects containing the __dataType tag and instantiates actual ES6 class objects (new Map(), new Set(), BigInt(), new Date()).
  • Lines 95โ€“105 (safeStringify): Uses an in-flight WeakSet to 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


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45ยฐC
INSPECTING DOM: VALID
TAGS: SCANNING...
+--------------------------------------------------------------------------+
| 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:

  1. Implement TypedStorage.set(key, value): Automatically serialize complex types with type metadata.
  2. Implement TypedStorage.get(key): Automatically revitalize objects with exact class prototypes.
  3. 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

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45ยฐC
INSPECTING DOM: VALID
TAGS: SCANNING...

โš ๏ธ Common Pitfalls

  1. Invoking JSON.stringify(bigint) Directly: BigInt does not have a .toJSON() method. Attempting to stringify a BigInt throws an immediate uncaught TypeError that will crash your application.
  2. Assuming Date Prototypes Persist: JSON.parse(JSON.stringify(new Date())) returns a string primitive, not a Date instance. Calling .getTime() on it will throw a TypeError.
  3. Unsanitized Object Keys (Prototype Pollution): When reviving untrusted JSON payloads containing keys like __proto__ or constructor, ensure your parser does not assign them directly to Object prototypes.

๐Ÿ’ก Pro Tips

  1. 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.
  2. Compression for Large Payloads: For payloads approaching 100KBโ€“500KB, evaluate compression libraries like lz-string (LZString.compressToUTF16(str)) before writing to localStorage to save up to 70% of quota space.

๐Ÿ“Œ Key Takeaways

  • JSON.stringify() natively fails to preserve Date, Map, Set, BigInt, and RegExp.
  • 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 WeakSet reference tracking during serialization.
  • Always protect JSON.parse() calls with try...catch to prevent unhandled syntax errors from corrupt strings.
  • --
โญ LEARN: HTML ๐ŸŒŸ โš”๏ธ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

What happens when you run JSON.stringify({ balance: 100000000000000000n }) without a custom replacer?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

How does JSON.stringify handle an ES6 Map by default?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

What does the second argument of JSON.parse(text, reviver) do?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP