Asher Cohen
Back to posts

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

TypeJSON.stringifystructuredClone
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 limitations
  • toJSON() — Custom serialization for your classes
  • replacer/reviver — Transform values during serialization/deserialization
  • structuredClone() — 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