Serialization in JavaScript: JSON and Beyond
Complete guide to serialization and deserialization in JavaScript — JSON, dates, custom objects, and common pitfalls
Introduction
Serialization is the process of converting an object or data structure into a format suitable for storage or transmission. In JavaScript, JSON.stringify() and JSON.parse() are the primary tools, but they come with important limitations you need to understand.
What is Serialization?
Serialization converts in-memory objects to a transferable format (string, binary, etc.).
Deserialization reconstructs objects from the serialized format.
// Serialize: object → string
const user = { name: 'Alice', age: 30 };
const json = JSON.stringify(user);
console.log(json); // '{"name":"Alice","age":30}'
// Deserialize: string → object
const parsed = JSON.parse(json);
console.log(parsed.name); // 'Alice'
JSON.stringify() Deep Dive
Basic Usage
// Simple objects
JSON.stringify({ x: 5, y: 6 });
// '{"x":5,"y":6}'
// Arrays
JSON.stringify([1, 'two', true, null]);
// '[1,"two",true,null]'
// Nested structures
JSON.stringify({
user: { name: 'Alice' },
items: [1, 2, 3]
});
// '{"user":{"name":"Alice"},"items":[1,2,3]}'
The replacer Parameter
// Filter which properties to include (array)
const user = { name: 'Alice', password: 'secret', age: 30 };
JSON.stringify(user, ['name', 'age']);
// '{"name":"Alice","age":30}' (password excluded)
// Transform values (function)
JSON.stringify(user, (key, value) => {
if (key === 'password') return undefined; // Exclude
if (key === 'name') return value.toUpperCase();
return value;
});
// '{"name":"ALICE","age":30}'
The space Parameter
const data = { name: 'Alice', items: [1, 2, 3] };
// Number: indent with N spaces
JSON.stringify(data, null, 2);
// {
// "name": "Alice",
// "items": [
// 1,
// 2,
// 3
// ]
// }
// String: use as indent
JSON.stringify(data, null, '\t');
// Tab-indented output
What JSON.stringify() Cannot Handle
const problematic = {
fn: function() {}, // ❌ Functions → omitted
undef: undefined, // ❌ undefined → omitted
sym: Symbol('test'), // ❌ Symbols → omitted
date: new Date(), // ⚠️ Dates → ISO string (not restored as Date)
regex: /test/, // ❌ RegExp → "{}"
map: new Map(), // ❌ Map → "{}"
set: new Set(), // ❌ Set → "{}"
big: 123n, // ❌ BigInt → TypeError!
circ: null // ❌ Circular refs → TypeError!
};
JSON.stringify(problematic);
// '{"date":"2024-01-01T00:00:00.000Z","regex":{}}'
Handling Special Types
Dates
// ❌ Dates lose their type
const user = { name: 'Alice', createdAt: new Date() };
const json = JSON.stringify(user);
// '{"name":"Alice","createdAt":"2024-01-01T00:00:00.000Z"}'
const parsed = JSON.parse(json);
console.log(parsed.createdAt instanceof Date); // false (it's a string!)
// ✅ Solution: custom reviver
const reviver = (key, value) => {
// Match ISO date strings
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
};
const parsed2 = JSON.parse(json, reviver);
console.log(parsed2.createdAt instanceof Date); // true
// ✅ Alternative: explicit date handling
const serialize = (obj) => JSON.stringify({
...obj,
createdAt: obj.createdAt.toISOString()
});
const deserialize = (json) => {
const obj = JSON.parse(json);
return { ...obj, createdAt: new Date(obj.createdAt) };
};
Maps and Sets
// ❌ Maps and Sets don't serialize
const data = {
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3])
};
// ✅ Convert to arrays before serializing
const serializable = {
map: [...data.map.entries()],
set: [...data.set]
};
const json = JSON.stringify(serializable);
// '{"map":[["key","value"]],"set":[1,2,3]}'
// ✅ Reconstruct after parsing
const parsed = JSON.parse(json);
const reconstructed = {
map: new Map(parsed.map),
set: new Set(parsed.set)
};
BigInt
// ❌ BigInt throws TypeError
const data = { id: 123n };
// JSON.stringify(data); // TypeError: Do not know how to serialize a BigInt
// ✅ Convert to string
const serializable = { id: data.id.toString() };
const json = JSON.stringify(serializable);
// '{"id":"123"}'
// ✅ Reconstruct
const parsed = JSON.parse(json);
const reconstructed = { id: BigInt(parsed.id) };
Custom toJSON() Method
// Objects can define their own serialization
class User {
constructor(name, password, createdAt) {
this.name = name;
this.password = password;
this.createdAt = createdAt;
}
toJSON() {
return {
name: this.name,
createdAt: this.createdAt.toISOString(),
type: 'user'
};
// password is excluded!
}
}
const user = new User('Alice', 'secret', new Date());
console.log(JSON.stringify(user));
// '{"name":"Alice","createdAt":"2024-01-01T00:00:00.000Z","type":"user"}'
JSON.parse() Deep Dive
The reviver Parameter
const json = '{"name":"Alice","birthDate":"1990-01-01","score":"100"}';
const parsed = JSON.parse(json, (key, value) => {
// Convert score to number
if (key === 'score') return Number(value);
// Convert date strings to Date objects
if (key === 'birthDate') return new Date(value);
return value;
});
console.log(parsed.score); // 100 (number, not string)
console.log(parsed.birthDate instanceof Date); // true
Error Handling
// ❌ Invalid JSON throws SyntaxError
try {
JSON.parse('{invalid}');
} catch (error) {
console.error('Invalid JSON:', error.message);
}
// ✅ Safe parse helper
const safeParse = (json, fallback = null) => {
try {
return JSON.parse(json);
} catch {
return fallback;
}
};
console.log(safeParse('{invalid}', {})); // {}
console.log(safeParse('{"valid":true}', {})); // { valid: true }
Circular References
// ❌ Circular references cause TypeError
const obj = { name: 'test' };
obj.self = obj;
// JSON.stringify(obj); // TypeError: Converting circular structure to JSON
// ✅ Solution 1: Remove circular refs before serializing
const { self, ...safe } = obj;
JSON.stringify(safe); // '{"name":"test"}'
// ✅ Solution 2: Use a library (flatted, circular-json)
import { stringify, parse } from 'flatted';
const json = stringify(obj); // Handles circular refs
const restored = parse(json);
Deep Clone with JSON
// Quick deep clone (with limitations)
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
const original = { a: 1, b: { c: 2 } };
const clone = deepClone(original);
clone.b.c = 999;
console.log(original.b.c); // 2 (unchanged - deep clone worked!)
// ⚠️ Limitations:
// - Loses functions, undefined, symbols
// - Dates become strings
// - Maps/Sets become empty objects
// - Fails on circular references
// - Fails on BigInt
// ✅ For proper deep cloning, use structuredClone()
const properClone = structuredClone(original);
structuredClone() — The Modern Alternative
// Built-in deep cloning (Node 17+, modern browsers)
const original = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
nested: { array: [1, 2, 3] }
};
const clone = structuredClone(original);
// ✅ Preserves:
// - Dates
// - Maps and Sets
// - ArrayBuffers
// - RegExp
// - Nested objects/arrays
// ❌ Still cannot clone:
// - Functions
// - Symbols
// - DOM nodes
// - Error objects (partially)
Common Patterns
API Response Handling
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const json = await response.json();
// Revive dates
return JSON.parse(JSON.stringify(json), (key, value) => {
if (key.endsWith('At') || key === 'birthDate') {
return new Date(value);
}
return value;
});
}
Local Storage
// Store complex data in localStorage
const save = (key, data) => {
localStorage.setItem(key, JSON.stringify(data));
};
const load = (key, fallback = null) => {
const json = localStorage.getItem(key);
if (!json) return fallback;
try {
return JSON.parse(json);
} catch {
return fallback;
}
};
// Usage
save('user-preferences', { theme: 'dark', fontSize: 16 });
const prefs = load('user-preferences', { theme: 'light', fontSize: 14 });
Query String Serialization
// Serialize object to query string
const toQueryString = (params) => {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
};
console.log(toQueryString({ name: 'Alice', age: 30 }));
// 'name=Alice&age=30'
// Deserialize query string to object
const fromQueryString = (qs) => {
return Object.fromEntries(
qs.replace('?', '').split('&').map(pair => pair.split('='))
);
};
Best Practices
Do's
// ✅ Use toJSON() for custom serialization
class Model {
toJSON() { return { id: this.id, type: 'model' }; }
}
// ✅ Use reviver for date handling
JSON.parse(json, (k, v) => isISODate(v) ? new Date(v) : v);
// ✅ Handle parse errors
try { JSON.parse(json); } catch { /* fallback */ }
// ✅ Use structuredClone for deep cloning
const clone = structuredClone(original);
// ✅ Convert Maps/Sets to arrays before serializing
const data = { map: [...myMap], set: [...mySet] };
Don'ts
// ❌ Don't serialize functions, undefined, symbols
// They're silently dropped or cause errors
// ❌ Don't use JSON for deep cloning complex objects
// Use structuredClone() instead
// ❌ Don't serialize circular references
// Remove them or use a library
// ❌ Don't forget BigInt can't be serialized
// Convert to string first
// ❌ Don't assume JSON.parse always succeeds
// Always wrap in try-catch
Summary Table
| Type | JSON.stringify | structuredClone |
|---|---|---|
| Objects | ✅ | ✅ |
| Arrays | ✅ | ✅ |
| Strings | ✅ | ✅ |
| Numbers | ✅ | ✅ |
| Booleans | ✅ | ✅ |
| null | ✅ | ✅ |
| Date | ⚠️ ISO string | ✅ |
| Map | ❌ {} | ✅ |
| Set | ❌ {} | ✅ |
| Function | ❌ omitted | ❌ error |
| undefined | ❌ omitted | ❌ error |
| Symbol | ❌ omitted | ❌ error |
| BigInt | ❌ TypeError | ❌ error |
| RegExp | ❌ {} | ✅ |
| Circular refs | ❌ TypeError | ❌ error |
Conclusion
Serialization is essential for data storage and transmission:
JSON.stringify()/JSON.parse()— The standard, with known limitationstoJSON()— Custom serialization for your classesreplacer/reviver— Transform values during serialization/deserializationstructuredClone()— Modern deep cloning that preserves more types- Always handle errors — Invalid JSON is common in real-world data
Understand what JSON can and cannot serialize, and you'll avoid the most common data handling bugs.
#javascript #json #serialization #data #web-development #best-practices