The 8 Types JSON.stringify Breaks
| Type | Input | JSON.stringify Output | Problem |
|---|---|---|---|
| Date | new 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 thrown | Crashes 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 |
| Circular | obj.self = obj | TypeError thrown | Crashes 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') };23const json = JSON.stringify(data);4// '{"createdAt":"2026-01-15T10:30:00.000Z"}'56const parsed = JSON.parse(json);7console.log(parsed.createdAt instanceof Date); // false8console.log(typeof parsed.createdAt); // "string"910// Fix: Use a reviver11const 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); // trueWarning
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 applications2const transaction = {3 id: 9007199254740993n, // Larger than Number.MAX_SAFE_INTEGER4 amount: 100n,5};67JSON.stringify(transaction);8// TypeError: Do not know how to serialize a BigInt910// Fix 1: Replacer converts BigInt to string11const json = JSON.stringify(transaction, (key, value) =>12 typeof value === 'bigint' ? value.toString() : value13);14// '{"id":"9007199254740993","amount":"100"}'1516// 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 arrays2JSON.stringify({ a: 1, b: undefined, c: 3 });3// '{"a":1,"c":3}' — key "b" is gone45JSON.stringify([1, undefined, 3]);6// '[1,null,3]' — undefined becomes null in arrays78// NaN and Infinity: silently become null9JSON.stringify({ score: NaN, max: Infinity, min: -Infinity });10// '{"score":null,"max":null,"min":null}'1112// Fix: Custom replacer13JSON.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};56JSON.stringify(data);7// '{"tags":{},"metadata":{}}' — ALL DATA LOST89// Fix: Manual conversion before stringify10JSON.stringify({11 tags: [...data.tags], // Set -> Array12 metadata: Object.fromEntries(data.metadata), // Map -> Object13});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 -> user45JSON.stringify(user);6// TypeError: Converting circular structure to JSON78// Fix: WeakSet-based replacer9function 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}1920safeStringify(user);21// '{"name":"Alice","team":{"name":"Engineering","lead":"[Circular]"}}'The replacer and reviver Pattern
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}910function 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}2223const data = {24 created: new Date('2026-01-15'),25 tags: new Set(['a', 'b']),26 count: 9007199254740993n,27};2829const json = JSON.stringify(data, replacer);30const restored = JSON.parse(json, reviver);3132console.log(restored.created instanceof Date); // true33console.log(restored.tags instanceof Set); // true34console.log(typeof restored.count); // "bigint"Library Solutions
| Library | Weekly Downloads | Bundle Size | Handles | Best For |
|---|---|---|---|---|
| superjson | ~3M | ~9 KB | Date, BigInt, Set, Map, RegExp, undefined, circular | tRPC, Next.js Server Actions |
| devalue | ~1.5M | ~5 KB | Date, BigInt, Set, Map, RegExp, circular, better perf | SvelteKit, general use |
| flatted | ~8M | ~0.5 KB | Circular references only | Minimal fix for circular refs |
superjson
superjson preserves all typesjavascript
1import superjson from 'superjson';23const data = {4 created: new Date('2026-01-15'),5 tags: new Set(['typescript', 'react']),6 count: 9007199254740993n,7 pattern: /^hello/i,8};910const { json, meta } = superjson.serialize(data);11// json contains the plain JSON-safe values12// meta tracks which fields need type restoration1314const restored = superjson.deserialize({ json, meta });15console.log(restored.created instanceof Date); // true16console.log(restored.tags instanceof Set); // true17console.log(typeof restored.count); // "bigint"18console.log(restored.pattern instanceof RegExp); // truedevalue
devalue: faster alternativejavascript
1import { stringify, parse } from 'devalue';23const data = {4 created: new Date('2026-01-15'),5 items: new Set([1, 2, 3]),6 lookup: new Map([['key', 'value']]),7};89const encoded = stringify(data);10const restored = parse(encoded);11// All types preserved, handles circular references12// ~30% faster than superjson for large payloadsNext.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 Component2async function UsersPage() {3 const users = await db.query('SELECT * FROM users');4 // users[0].createdAt is a Date object from the database56 return <UserList users={users} />;7 // Props are serialized to JSON for the client8 // users[0].createdAt becomes a STRING, not a Date9}1011// components/UserList.tsx — Client Component12'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); // false1718 // SOLUTION 1: Serialize dates as ISO strings explicitly19 // In server component: users.map(u => ({ ...u, createdAt: u.createdAt.toISOString() }))20 // In client component: new Date(user.createdAt)2122 // SOLUTION 2: Use next-superjson-plugin23 // npm install next-superjson-plugin superjson24 // 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};910// JSON round-trip: LOSES everything11const 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)1516// structuredClone: PRESERVES types17const clone = structuredClone(original);18console.log(clone.date instanceof Date); // true19console.log(clone.regex instanceof RegExp); // true20console.log(clone.set instanceof Set); // true21console.log(clone.map instanceof Map); // true2223// structuredClone also handles circular references!24const obj = { name: 'test' };25obj.self = obj;26const safe = structuredClone(obj); // Works fineNote
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
structuredCloneinstead ofJSON.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.toJSONglobally in a library — only in application code - ✗Avoid regex-based date revivers that match any ISO-like string — they cause false positives
Try These Tools
Continue Learning
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.