Learn/Advanced Topics

JSON Serialization Edge Cases

JSON.stringify() silently drops undefined, throws on BigInt, converts Dates to strings that never come back, and crashes on circular references. This guide covers every edge case and the libraries that fix them.

The 8 Types JSON.stringify Breaks

JSON.stringify Failure Modes
TypeInputJSON.stringify OutputProblem
Datenew Date("2026-01-15")"2026-01-15T00:00:00.000Z"Becomes a string, never restored as Date
undefined{ a: 1, b: undefined }{"a":1}Key silently removed from output
BigInt{ id: 9007199254740993n }TypeError thrownCrashes the entire serialization
NaN{ score: NaN }{"score":null}Silently becomes null
Infinity{ max: Infinity }{"max":null}Silently becomes null
Set{ tags: new Set(["a","b"]) }{"tags":{}}Becomes empty object, data lost
Map{ map: new Map([["k","v"]]) }{"map":{}}Becomes empty object, data lost
Circularobj.self = objTypeError thrownCrashes with circular reference error

1. Date Serialization

The most common edge case. JSON.stringify calls the Date's toISOString() method, producing a string. JSON.parse does not know this was originally a Date:

Date round-trip failurejavascript
1const data = { createdAt: new Date('2026-01-15T10:30:00Z') };
2
3const json = JSON.stringify(data);
4// '{"createdAt":"2026-01-15T10:30:00.000Z"}'
5
6const parsed = JSON.parse(json);
7console.log(parsed.createdAt instanceof Date); // false
8console.log(typeof parsed.createdAt); // "string"
9
10// Fix: Use a reviver
11const restored = JSON.parse(json, (key, value) => {
12 if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
13 const date = new Date(value);
14 if (!isNaN(date.getTime())) return date;
15 }
16 return value;
17});
18console.log(restored.createdAt instanceof Date); // true

Warning

The ISO date reviver pattern above is fragile — it will convert any string that looks like a date, including values that are intentionally strings (like user input or IDs). Prefer explicit field-level conversion or use a library like superjson that tracks types with metadata.

2. BigInt: The Hard Crash

BigInt throws TypeErrorjavascript
1// BigInt is common in financial and analytics applications
2const transaction = {
3 id: 9007199254740993n, // Larger than Number.MAX_SAFE_INTEGER
4 amount: 100n,
5};
6
7JSON.stringify(transaction);
8// TypeError: Do not know how to serialize a BigInt
9
10// Fix 1: Replacer converts BigInt to string
11const json = JSON.stringify(transaction, (key, value) =>
12 typeof value === 'bigint' ? value.toString() : value
13);
14// '{"id":"9007199254740993","amount":"100"}'
15
16// Fix 2: BigInt.prototype.toJSON (global, use cautiously)
17BigInt.prototype.toJSON = function () { return this.toString(); };
18JSON.stringify(transaction);
19// '{"id":"9007199254740993","amount":"100"}'

Important

When sending BigInt as strings over APIs, the receiving side must know to parse them back to BigInt (or use a big number library). Document which fields contain BigInt values in your API schema. Better yet, use superjson to preserve the type automatically.

3. undefined, NaN, and Infinity

Silent data lossjavascript
1// undefined: silently dropped from objects, becomes null in arrays
2JSON.stringify({ a: 1, b: undefined, c: 3 });
3// '{"a":1,"c":3}' — key "b" is gone
4
5JSON.stringify([1, undefined, 3]);
6// '[1,null,3]' — undefined becomes null in arrays
7
8// NaN and Infinity: silently become null
9JSON.stringify({ score: NaN, max: Infinity, min: -Infinity });
10// '{"score":null,"max":null,"min":null}'
11
12// Fix: Custom replacer
13JSON.stringify(data, (key, value) => {
14 if (value === undefined) return '__undefined__';
15 if (Number.isNaN(value)) return '__NaN__';
16 if (value === Infinity) return '__Infinity__';
17 if (value === -Infinity) return '__-Infinity__';
18 return value;
19});

4. Set and Map: Silent Empty Objects

Set and Map vanish silentlyjavascript
1const data = {
2 tags: new Set(['typescript', 'react', 'nextjs']),
3 metadata: new Map([['version', '2.0'], ['author', 'Alice']]),
4};
5
6JSON.stringify(data);
7// '{"tags":{},"metadata":{}}' — ALL DATA LOST
8
9// Fix: Manual conversion before stringify
10JSON.stringify({
11 tags: [...data.tags], // Set -> Array
12 metadata: Object.fromEntries(data.metadata), // Map -> Object
13});
14// '{"tags":["typescript","react","nextjs"],"metadata":{"version":"2.0","author":"Alice"}}'

5. Circular References

Circular reference crashjavascript
1const user = { name: 'Alice' };
2const team = { name: 'Engineering', lead: user };
3user.team = team; // Circular: user -> team -> user
4
5JSON.stringify(user);
6// TypeError: Converting circular structure to JSON
7
8// Fix: WeakSet-based replacer
9function safeStringify(obj) {
10 const seen = new WeakSet();
11 return JSON.stringify(obj, (key, value) => {
12 if (typeof value === 'object' && value !== null) {
13 if (seen.has(value)) return '[Circular]';
14 seen.add(value);
15 }
16 return value;
17 });
18}
19
20safeStringify(user);
21// '{"name":"Alice","team":{"name":"Engineering","lead":"[Circular]"}}'

The replacer and reviver Pattern

Custom Serialization Round-Trip
Complete replacer/reviver implementationjavascript
1function replacer(key, value) {
2 if (value instanceof Date) return { __type: 'Date', value: value.toISOString() };
3 if (value instanceof Set) return { __type: 'Set', value: [...value] };
4 if (value instanceof Map) return { __type: 'Map', value: [...value] };
5 if (typeof value === 'bigint') return { __type: 'BigInt', value: value.toString() };
6 if (value === undefined) return { __type: 'undefined' };
7 return value;
8}
9
10function reviver(key, value) {
11 if (value && typeof value === 'object' && '__type' in value) {
12 switch (value.__type) {
13 case 'Date': return new Date(value.value);
14 case 'Set': return new Set(value.value);
15 case 'Map': return new Map(value.value);
16 case 'BigInt': return BigInt(value.value);
17 case 'undefined': return undefined;
18 }
19 }
20 return value;
21}
22
23const data = {
24 created: new Date('2026-01-15'),
25 tags: new Set(['a', 'b']),
26 count: 9007199254740993n,
27};
28
29const json = JSON.stringify(data, replacer);
30const restored = JSON.parse(json, reviver);
31
32console.log(restored.created instanceof Date); // true
33console.log(restored.tags instanceof Set); // true
34console.log(typeof restored.count); // "bigint"

Library Solutions

LibraryWeekly DownloadsBundle SizeHandlesBest For
superjson~3M~9 KBDate, BigInt, Set, Map, RegExp, undefined, circulartRPC, Next.js Server Actions
devalue~1.5M~5 KBDate, BigInt, Set, Map, RegExp, circular, better perfSvelteKit, general use
flatted~8M~0.5 KBCircular references onlyMinimal fix for circular refs

superjson

superjson preserves all typesjavascript
1import superjson from 'superjson';
2
3const data = {
4 created: new Date('2026-01-15'),
5 tags: new Set(['typescript', 'react']),
6 count: 9007199254740993n,
7 pattern: /^hello/i,
8};
9
10const { json, meta } = superjson.serialize(data);
11// json contains the plain JSON-safe values
12// meta tracks which fields need type restoration
13
14const restored = superjson.deserialize({ json, meta });
15console.log(restored.created instanceof Date); // true
16console.log(restored.tags instanceof Set); // true
17console.log(typeof restored.count); // "bigint"
18console.log(restored.pattern instanceof RegExp); // true

devalue

devalue: faster alternativejavascript
1import { stringify, parse } from 'devalue';
2
3const data = {
4 created: new Date('2026-01-15'),
5 items: new Set([1, 2, 3]),
6 lookup: new Map([['key', 'value']]),
7};
8
9const encoded = stringify(data);
10const restored = parse(encoded);
11// All types preserved, handles circular references
12// ~30% faster than superjson for large payloads

Next.js Server Component Serialization

Next.js Server Components automatically serialize props to the client using JSON. This means any Date, Set, or Map in your server data breaks silently:

The Server Component serialization boundarytypescript
1// app/users/page.tsx — Server Component
2async function UsersPage() {
3 const users = await db.query('SELECT * FROM users');
4 // users[0].createdAt is a Date object from the database
5
6 return <UserList users={users} />;
7 // Props are serialized to JSON for the client
8 // users[0].createdAt becomes a STRING, not a Date
9}
10
11// components/UserList.tsx — Client Component
12'use client';
13function UserList({ users }) {
14 // PROBLEM: users[0].createdAt is now a string!
15 const date = users[0].createdAt;
16 console.log(date instanceof Date); // false
17
18 // SOLUTION 1: Serialize dates as ISO strings explicitly
19 // In server component: users.map(u => ({ ...u, createdAt: u.createdAt.toISOString() }))
20 // In client component: new Date(user.createdAt)
21
22 // SOLUTION 2: Use next-superjson-plugin
23 // npm install next-superjson-plugin superjson
24 // Add to next.config.js:
25 // experimental: { swcPlugins: [['next-superjson-plugin', {}]] }
26}

Recommended Approach for Next.js

For a few date fields, convert them to ISO strings on the server and parse them on the client explicitly. This is the simplest, most transparent approach. For complex data with many types (Sets, Maps, BigInts), use next-superjson-plugin to handle serialization automatically across the Server/Client boundary.

structuredClone vs JSON Round-Trip

structuredClone preserves types, JSON doesn'tjavascript
1const original = {
2 date: new Date('2026-01-15'),
3 regex: /hello/gi,
4 set: new Set([1, 2, 3]),
5 map: new Map([['a', 1]]),
6 buffer: new Uint8Array([1, 2, 3]),
7 error: new Error('test'),
8};
9
10// JSON round-trip: LOSES everything
11const jsonClone = JSON.parse(JSON.stringify(original));
12console.log(jsonClone.date instanceof Date); // false (string)
13console.log(jsonClone.regex instanceof RegExp); // false (empty object)
14console.log(jsonClone.set instanceof Set); // false (empty object)
15
16// structuredClone: PRESERVES types
17const clone = structuredClone(original);
18console.log(clone.date instanceof Date); // true
19console.log(clone.regex instanceof RegExp); // true
20console.log(clone.set instanceof Set); // true
21console.log(clone.map instanceof Map); // true
22
23// structuredClone also handles circular references!
24const obj = { name: 'test' };
25obj.self = obj;
26const safe = structuredClone(obj); // Works fine

Note

structuredClone is available in all modern browsers (Chrome 98+, Firefox 94+, Safari 15.4+) and Node.js 17+. It cannot clone functions, DOM nodes, or class instances with methods. For deep cloning data objects, it is strictly superior to the JSON round-trip hack.

Best Practices

  • Use structuredClone instead of JSON.parse(JSON.stringify()) for deep cloning
  • Use superjson or devalue when you need type-safe serialization across network boundaries
  • Send BigInt values as strings in APIs and document which fields use this convention
  • Convert Dates to ISO 8601 strings explicitly at the API boundary
  • Use TypeScript to enforce serialization contracts across client/server boundaries
  • Never assume JSON.parse(JSON.stringify(obj)) produces an equivalent object
  • Never patch BigInt.prototype.toJSON globally in a library — only in application code
  • Avoid regex-based date revivers that match any ISO-like string — they cause false positives

Frequently Asked Questions

Why does JSON.stringify drop undefined values?
The JSON specification has no concept of undefined — it only defines null, boolean, number, string, array, and object. When JSON.stringify encounters undefined as an object property value, it silently omits the key entirely. When undefined appears in an array, it is replaced with null. This is by design per the ECMAScript specification.
How do I serialize BigInt to JSON?
JSON.stringify throws a TypeError on BigInt values because the JSON spec has no BigInt type. Solutions: (1) use a replacer function to convert BigInt to string, (2) use superjson which preserves BigInt with metadata, or (3) send BigInt values as strings in your API and parse them on the client.
What is superjson and when should I use it?
superjson is a library (~3M weekly npm downloads) that extends JSON to preserve JavaScript types like Date, BigInt, Set, Map, RegExp, and undefined. It serializes data alongside type metadata so the original types can be restored on deserialization. Use it with tRPC, Next.js Server Actions, or any client-server boundary where type fidelity matters.
How do I handle circular references in JSON?
JSON.stringify throws "Converting circular structure to JSON" on circular references. Options: (1) use the flatted library (~8M weekly downloads) which replaces cycles with reference pointers, (2) use superjson which also handles circular refs, or (3) use a replacer function with a WeakSet to detect and skip circular references.
Is structuredClone better than JSON.parse(JSON.stringify()) for deep cloning?
Yes. structuredClone (available in all modern browsers and Node.js 17+) preserves Date, RegExp, Map, Set, ArrayBuffer, Error, and more. The JSON round-trip loses all of these types. structuredClone also handles circular references. The only advantage of JSON.parse(JSON.stringify()) is that it works in very old environments.
Why do Date objects break in Next.js Server Components?
Next.js Server Components serialize props to the client using JSON. Since JSON.stringify converts Date objects to ISO strings that are never automatically restored as Date objects, your client components receive strings where they expect Dates. Use superjson (via next-superjson-plugin), serialize dates as ISO strings explicitly, or use timestamps.
How does the replacer and reviver pattern work?
JSON.stringify accepts a second argument (replacer) that transforms values during serialization. JSON.parse accepts a second argument (reviver) that transforms values during parsing. Together, they let you implement custom serialization: the replacer marks special types with a convention (e.g., "$date:2026-01-01T00:00:00Z") and the reviver detects the marker and restores the original type.