Deep Dive into JSON, Parsing & Serialization

Reviewed & published by Brayan K

JSON (JavaScript Object Notation) is a lightweight, text-based data format for storing and exchanging structured data, which you turn into JavaScript values with JSON.parse() and back into text with JSON.stringify().

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience: Download Node.js to run JavaScript on your computer, use your browser's Developer Console (Press F12) to test code snippets, or create a .html file with <script> tags and open it in your browser.

Master JSON handling: serialization, parsing, type preservation, circular references, validation, and production patterns.

What You'll Learn

📦 Real-World Analogy: Universal Shipping Label

📋 JSON Data Types Quick Reference:

TypeJSON ExampleNotes
String"hello"Must use double quotes
Number42, 3.14No NaN or Infinity
Booleantrue, falseLowercase only
Nullnullundefined is dropped from objects, but becomes null inside arrays
Array[1, 2, 3]Ordered list
ObjectKeys must be strings

What JSON Actually Is (And What It Cannot Represent)

JSON (JavaScript Object Notation) is a text-based data format, not JavaScript itself. It has strict rules and can only represent: objects, arrays, strings, numbers, booleans, and null.

// What JSON can represent
const validData = {
  string: "Hello",
  number: 42,
  boolean: true,
  null: null,
  array: [1, 2, 3],
  object: { nested: "value" }
};

console.log(JSON.stringify(validData, null, 2));

// What JSON CANNOT represent (these get lost or converted)
const problematicData = {
  undefined: undefined,        // ❌ Removed entirely
  function: function() {},     // ❌ Removed entirely
  date: new Date(),            // ⚠️ Becomes ISO string
  nan: NaN,                    // ⚠️ Becomes null
  infinity: Infinity,          // ⚠️ Becomes null
  regex: /pattern/g,           // ❌ Becomes empty object {}
  map: new Map([["a", 1]]),    // ❌ Becomes empty object {}
  set: new Set([1, 2, 3])      // ❌ Becomes empty object {}
};

console.log("\nProblematic data after stringify:");
console.log(JSON.stringify(problematicData, null, 2));
// Notice: undefined, function, regex, map, set are GONE!

JSON.stringify — Serialization

Serialization converts JavaScript values into JSON text. Understanding how different types are handled is crucial to avoiding data loss.

// Basic serialization
const user = { name: "Boopie", age: 25 };
const json = JSON.stringify(user);
console.log(json);
console.log(typeof json); // "string"

// Pretty-printing with indentation
console.log("\nPretty-printed:");
console.log(JSON.stringify(user, null, 2));

// Date conversion (becomes ISO string)
const withDate = { created: new Date() };
console.log("\nDate becomes string:");
console.log(JSON.stringify(withDate));

// NaN and Infinity become null
const special = { nan: NaN, inf: Infinity, negInf: -Infinity };
console.log("\nSpecial numbers become null:");
console.log(JSON.stringify(special));

// undefined is removed from objects
const withUndefined = { a: 1, b: undefined, c: 3 };
console.log("\nUndefined removed:");
console.log(JSON.stringify(withUndefined)); // {"a":1,"c":3}

JSON.parse — Deserialization

Parsing converts JSON text back to JavaScript values. JSON syntax is extremely strict — errors that JavaScript allows will crash JSON.parse.

// Basic parsing
const jsonText = '{"name":"Boopie","score":100}';
const obj = JSON.parse(jsonText);
console.log(obj.name);  // "Boopie"
console.log(obj.score); // 100

// STRICT syntax rules - these will FAIL:
try {
  // Keys must be double-quoted
  JSON.parse("{name: 'value'}");
} catch (e) {
  console.log("Error 1:", e.message);
}

try {
  // Strings must use double quotes, not single
  JSON.parse('{"name": \'value\'}');
} catch (e) {
  console.log("Error 2:", e.message);
}

try {
  // No trailing commas allowed
  JSON.parse('{"a": 1, "b": 2,}');
} catch (e) {
  console.log("Error 3:", e.message);
}

// Safe parsing wrapper
function safeParse(str) {
  try {
    return JSON.parse(str);
  } catch {
    return null;
  }
}

console.log("\nSafe parse invalid:", safeParse("not json"));
console.log("Safe parse valid:", safeParse('{"valid": true}'));

Custom Serialization with Replacers

The replacer parameter in JSON.stringify lets you filter keys, transform values, mask sensitive data, and preserve type information.

// Replacer as array - filter specific keys only
const user = {
  id: 1,
  name: "Boopie",
  password: "secret123",
  email: "[email protected]"
};

// Only include safe fields
const safeJson = JSON.stringify(user, ["id", "name", "email"]);
console.log("Filtered:", safeJson);

// Replacer as function - transform values
const data = {
  name: "Boopie",
  password: "secret123",
  createdAt: new Date(),
  balance: 1234.567
};

const transformed = JSON.stringify(data, (key, value) => {
  // Mask sensitive fields
  if (key === "password") return "***HIDDEN***";
  
  // Format dates with type marker for revival
  if (value instanceof Date) {
    return { __type: "Date", value: value.toISOString() };
  }
  
  // Round numbers
  if (typeof value === "number" && key === "balance") {
    return Math.round(value * 100) / 100;
  }
  
  return value;
}, 2);

console.log("\nTransformed:");
console.log(transformed);

Custom Parsing with Revivers

The reviver parameter in JSON.parse lets you restore Dates, class instances, and other types that JSON destroys during serialization.

// Reviver function to restore types
const jsonWithDate = '{"name":"Boopie","created":{"__type":"Date","value":"2025-01-15T10:30:00.000Z"}}';

const restored = JSON.parse(jsonWithDate, (key, value) => {
  // Restore Date objects
  if (value && value.__type === "Date") {
    return new Date(value.value);
  }
  return value;
});

console.log("Name:", restored.name);
console.log("Created type:", restored.created.constructor.name);
console.log("Is Date?", restored.created instanceof Date);
console.log("Year:", restored.created.getFullYear());

// Auto-detect ISO date strings
const autoDateJson = '{"event":"Meeting","time":"2025-06-15T14:00:00.000Z"}';

const autoRestored = JSON.parse(autoDateJson, (key, value) => {
  // Detect ISO date strings
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    const date = new Date(value);
    if (!isNaN(date.getTime())) return date;
  }
  return value;
});

console.log("\nAuto-restored time:", autoRestored.time);
console.log("Is Date?", autoRestored.time instanceof Date);

Worked Example — The Whole Round Trip

You have now seen stringify, parse, replacers and revivers separately. Here they are in one program, on one object, so you can see exactly what a round trip costs you and how to get it back. Read the comments first, then press Run and compare with the expected output at the bottom of the code.

// -- WORKED EXAMPLE - a full JSON round trip, and what it costs you --

// The object you have in memory. Note the deliberate mix of types.
const order = {
  id: 7,
  customer: "Priya",
  total: 129.5,
  paid: true,
  coupon: null,
  placedAt: new Date("2025-03-04T09:15:00.000Z"),   // a real Date object
  tags: ["priority", "gift"],
  internalNote: undefined,                          // will not survive
  recalculate: function () { return this.total; },  // will not survive
  seenBy: new Set(["web"])                          // will not survive as a Set
};

// STEP 1 - stringify. The third argument (2) is the indent, for readability.
const json = JSON.stringify(order, null, 2);
console.log(json);

// What happened to each awkward value:
//   placedAt     -> an ISO string, because Date has a built-in toJSON()
//   internalNote -> gone. undefined keys are dropped from objects entirely
//   recalculate  -> gone. Functions cannot be represented in JSON at all
//   seenBy       -> {} . A Set has no toJSON, so it serialises as an empty object

// STEP 2 - parse it back and inspect the damage.
const revived = JSON.parse(json);

console.log("id is still a:", typeof revived.id);
console.log("placedAt is now a:", typeof revived.placedAt);
console.log("is it a Date?", revived.placedAt instanceof Date);
console.log("internalNote survived?", "internalNote" in revived);
console.log("seenBy is now:", JSON.stringify(revived.seenBy));

// STEP 3 - repair the Date with a REVIVER. JSON.parse calls this for every
// key/value pair, and whatever you return replaces that value.
const isIsoDate = (v) => typeof v === "string" && /^\d{4}-\d{2}-\d{2}T/.test(v);

const properlyRevived = JSON.parse(json, (key, value) => {
  if (isIsoDate(value)) return new Date(value);   // string turns back into a Date
  return value;                                   // everything else passes through
});

console.log("after the reviver, is it a Date?", properlyRevived.placedAt instanceof Date);
console.log("year:", properlyRevived.placedAt.getUTCFullYear());

// STEP 4 - a REPLACER is the same idea in the other direction. Here it hides a
// field that must never reach a log file or an API.
const withSecret = { user: "Priya", password: "hunter2", role: "admin" };
console.log(JSON.stringify(withSecret, (key, value) =>
  key === "password" ? "***" : value
));

// Expected output:
// {
//   "id": 7,
//   "customer": "Priya",
//   "total": 129.5,
//   "paid": true,
//   "coupon": null,
//   "placedAt": "2025-03-04T09:15:00.000Z",
//   "tags": [
//     "priority",
//     "gift"
//   ],
//   "seenBy": {}
// }
// id is still a: number
// placedAt is now a: string
// is it a Date? false
// internalNote survived? false
// seenBy is now: {}
// after the reviver, is it a Date? true
// year: 2025
// {"user":"Priya","password":"***","role":"admin"}

The seenBy: {} line is the one that catches people in production. A Set does not error, does not warn, and does not vanish — it quietly turns into an empty object, and the bug shows up much later as "why is my list empty?".

🎯 Your Turn — Save, Load and Restore Settings

Three blanks, and they are the three things you will do with JSON every single day: write it out, read it back without crashing, and turn a date string into a real date again.

// 🎯 YOUR TURN - stringify, parse safely, then revive
// Everything is written for you except the three blanks marked ___

const settings = {
  theme: "dark",
  fontSize: 14,
  lastOpened: new Date("2025-11-02T08:00:00.000Z"),
  temp: undefined              // watch this one disappear
};

// 👉 1) Turn 'settings' into JSON text indented by 2 spaces.
//       JSON.stringify takes (value, replacer, indent) - pass null for replacer.
const json = ___;
console.log(json);

// 👉 2) Finish safeParse so bad input returns null instead of crashing the page.
//       Replace ___ with the statement that parses 'text' and hands it back.
function safeParse(text) {
  try {
    ___;
  } catch (err) {
    console.log("bad JSON:", err.name);
    return null;
  }
}

console.log("valid   ->", JSON.stringify(safeParse(json)));
console.log("invalid ->", safeParse("{oops}"));

// 👉 3) The reviver runs for every key/value pair. Return a real Date object
//       for lastOpened, built from the ISO string in 'value'.
const restored = JSON.parse(json, (key, value) => {
  if (key === "lastOpened") return ___;
  return value;
});

console.log("is a Date?", restored.lastOpened instanceof Date);
console.log("month (0-11):", restored.lastOpened.getUTCMonth());

// ✅ Expected output:
// {
//   "theme": "dark",
//   "fontSize": 14,
//   "lastOpened": "2025-11-02T08:00:00.000Z"
// }
// valid   -> {"theme":"dark","fontSize":14,"lastOpened":"2025-11-02T08:00:00.000Z"}
// bad JSON: SyntaxError
// invalid -> null
// is a Date? true
// month (0-11): 10

Two things worth noticing in the expected output. temp is missing from the JSON, because its value was undefined. And bad JSON: SyntaxError prints before invalid -> null, because JavaScript has to evaluate safeParse(...) before it can log the line containing it.

Handling Circular References

Circular references (objects referencing themselves) crash JSON.stringify. You need a custom serializer to handle them safely.

// Circular references break JSON.stringify
const obj = { name: "Parent" };
obj.self = obj; // Circular!

try {
  JSON.stringify(obj);
} catch (e) {
  console.log("Error:", e.message);
}

// Safe stringify that handles circular references
function safeStringify(obj, space = 2) {
  const seen = new WeakSet();
  
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return "[Circular Reference]";
      }
      seen.add(value);
    }
    return value;
  }, space);
}

// Test with circular structure
const circular = { name: "Node" };
circular.child = { parent: circular };

console.log("\nSafe circular stringify:");
console.log(safeStringify(circular));

// Complex nested circular
const a = { id: "A" };
const b = { id: "B", ref: a };
a.ref = b;

console.log("\nMutual circular refs:");
console.log(safeStringify(a));

Serializing Class Instances

When you serialize class instances, all methods are lost. Here's how to preserve and restore class types through JSON.

// Classes lose their methods when serialized
class User {
  constructor(name, role) {
    this.name = name;
    this.role = role;
  }
  
  greet() {
    return `Hello, I'm ${this.name}`;
  }
  
  isAdmin() {
    return this.role === "admin";
  }
}

const user = new User("Boopie", "admin");
console.log("Before serialize:");
console.log("greet():", user.greet());
console.log("isAdmin():", user.isAdmin());

// After JSON round-trip, methods are GONE
const json = JSON.stringify(user);
const parsed = JSON.parse(json);

console.log("\nAfter JSON round-trip:");
console.log("Data:", parsed);
console.log("Has greet?", typeof parsed.greet); // undefined!

// Solution: Custom serialization with type markers
function serializeWithType(obj) {
  return JSON.stringify(obj, (key, value) => {
    if (value instanceof User) {
      return { __class: "User", data: { name: value.name, role: value.role } };
    }
    return value;
  });
}

function deserializeWithType(json) {
  return JSON.parse(json, (key, value) => {
    if (value && value.__class === "User") {
      return new User(value.data.name, value.data.role);
    }
    return value;
  });
}

const serialized = serializeWithType(user);
console.log("\nSerialized with type:", serialized);

const revived = deserializeWithType(serialized);
console.log("\nRevived user:");
console.log("greet():", revived.greet());
console.log("isAdmin():", revived.isAdmin());

JSON Schema Validation

Never trust incoming JSON blindly. Always validate the structure, types, and required fields before using data in your application.

// Manual schema validation (without external libraries)
function validateUserSchema(data) {
  const errors = [];
  
  if (typeof data !== "object" || data === null) {
    return { valid: false, errors: ["Must be an object"] };
  }
  
  // Required fields
  if (typeof data.id !== "number") {
    errors.push("id must be a number");
  }
  if (typeof data.name !== "string" || data.name.length === 0) {
    errors.push("name must be a non-empty string");
  }
  if (typeof data.email !== "string" || !data.email.includes("@")) {
    errors.push("email must be a valid email");
  }
  
  // Optional fields with type checking
  if (data.age !== undefined && typeof data.age !== "number") {
    errors.push("age must be a number if provided");
  }
  if (data.roles !== undefined && !Array.isArray(data.roles)) {
    errors.push("roles must be an array if provided");
  }
  
  return {
    valid: errors.length === 0,
    errors
  };
}

// Test cases
const validUser = { id: 1, name: "Boopie", email: "[email protected]", age: 25 };
const invalidUser = { id: "wrong", name: "", email: "invalid" };

console.log("Valid user:", validateUserSchema(validUser));
console.log("\nInvalid user:", validateUserSchema(invalidUser));

// Safe parse with validation
function parseAndValidate(json, validator) {
  try {
    const data = JSON.parse(json);
    const result = validator(data);
    if (!result.valid) {
      return { success: false, errors: result.errors };
    }
    return { success: true, data };
  } catch (e) {
    return { success: false, errors: ["Invalid JSON: " + e.message] };
  }
}

const result = parseAndValidate('{"id":1,"name":"Test","email":"[email protected]"}', validateUserSchema);
console.log("\nParse & validate result:", result);

JSON Transform Pipeline

Professional applications use pipelines to process JSON: safe parse → validate → transform → normalize. This pattern keeps data handling consistent and safe.

// Professional JSON processing pipeline
class JSONPipeline {
  constructor() {
    this.transformers = [];
  }
  
  addTransformer(fn) {
    this.transformers.push(fn);
    return this; // chainable
  }
  
  process(json) {
    // Step 1: Safe parse
    let data;
    try {
      data = JSON.parse(json);
    } catch (e) {
      return { success: false, error: "Parse failed: " + e.message };
    }
    
    // Step 2: Run transformers
    try {
      for (const transform of this.transformers) {
        data = transform(data);
      }
    } catch (e) {
      return { success: false, error: "Transform failed: " + e.message };
    }
    
    return { success: true, data };
  }
}

// Create pipeline with transformers
const userPipeline = new JSONPipeline()
  // Normalize strings
  .addTransformer(data => ({
    ...data,
    name: data.name?.trim(),
    email: data.email?.toLowerCase().trim()
  }))
  // Add defaults
  .addTransformer(data => ({
    ...data,
    role: data.role || "user",
    active: data.active ?? true
  }))
  // Parse dates
  .addTransformer(data => ({
    ...data,
    createdAt: data.createdAt ? new Date(data.createdAt) : new Date()
  }));

const rawJson = '{"name":"  Boopie  ","email":"[email protected]","createdAt":"2025-01-15T10:00:00Z"}';
const result = userPipeline.process(rawJson);

console.log("Pipeline result:");
console.log(JSON.stringify(result, null, 2));
console.log("\nCreatedAt is Date?", result.data.createdAt instanceof Date);

JSON Versioning & Migrations

As your data schema evolves, you need a migration strategy to upgrade old JSON to new formats without breaking existing data.

// JSON versioning for backward compatibility
const migrations = {
  // v1 -> v2: renamed 'username' to 'name'
  1: (data) => ({
    ...data,
    version: 2,
    name: data.username,
    username: undefined
  }),
  
  // v2 -> v3: nested profile object
  2: (data) => ({
    version: 3,
    profile: {
      name: data.name,
      email: data.email
    },
    settings: data.settings || {}
  })
};

function migrateToLatest(data) {
  const LATEST_VERSION = 3;
  let current = { ...data };
  
  // Default version 1 for old data without version
  if (!current.version) {
    current.version = 1;
  }
  
  // Apply migrations sequentially
  while (current.version < LATEST_VERSION) {
    const migrate = migrations[current.version];
    if (!migrate) {
      throw new Error(`No migration for version ${current.version}`);
    }
    console.log(`Migrating v${current.version} -> v${current.version + 1}`);
    current = migrate(current);
  }
  
  return current;
}

// Test with old v1 data
const oldData = { username: "boopie", email: "[email protected]" };
console.log("Original (v1):", oldData);

const migrated = migrateToLatest(oldData);
console.log("\nMigrated to v3:", JSON.stringify(migrated, null, 2));

// v2 data only needs one migration
const v2Data = { version: 2, name: "boopie", email: "[email protected]" };
console.log("\nV2 data:", v2Data);
console.log("Migrated:", JSON.stringify(migrateToLatest(v2Data), null, 2));

Safe JSON Storage (localStorage)

Storing JSON in localStorage requires error handling, expiration support, and protection against corrupted data.

// Safe localStorage with JSON
const storage = {
  set(key, value, ttl = null) {
    const item = {
      value,
      timestamp: Date.now(),
      expires: ttl ? Date.now() + ttl : null
    };
    try {
      localStorage.setItem(key, JSON.stringify(item));
      return true;
    } catch (e) {
      console.error("Storage error:", e.message);
      return false;
    }
  },
  
  get(key) {
    try {
      const raw = localStorage.getItem(key);
      if (!raw) return null;
      
      const item = JSON.parse(raw);
      
      // Check expiration
      if (item.expires && Date.now() > item.expires) {
        localStorage.removeItem(key);
        return null;
      }
      
      return item.value;
    } catch {
      // Corrupted JSON
      localStorage.removeItem(key);
      return null;
    }
  },
  
  remove(key) {
    localStorage.removeItem(key);
  }
};

// Demo (simulated for this environment)
console.log("Storage utility demo:");

// Simulating localStorage behavior
const mockStorage = {};
const mockSet = (k, v) => { mockStorage[k] = JSON.stringify(v); };
const mockGet = (k) => mockStorage[k] ? JSON.parse(mockStorage[k]) : null;

mockSet("user", { value: { name: "Boopie" }, timestamp: Date.now() });
console.log("Stored user:", mockGet("user"));

mockSet("temp", { value: "data", expires: Date.now() + 60000 });
console.log("Stored with TTL:", mockGet("temp"));

Deep Cloning with JSON

The JSON.parse(JSON.stringify(obj)) pattern creates deep clones, but has significant limitations. Know when to use it and when to use structuredClone.

// JSON cloning - simple but has limitations
const original = {
  name: "Boopie",
  scores: [100, 95, 88],
  settings: { theme: "dark" }
};

// JSON clone method
const jsonClone = JSON.parse(JSON.stringify(original));

// Modify clone - original is safe
jsonClone.scores.push(75);
jsonClone.settings.theme = "light";

console.log("Original scores:", original.scores);
console.log("Clone scores:", jsonClone.scores);
console.log("Original theme:", original.settings.theme);
console.log("Clone theme:", jsonClone.settings.theme);

// BUT... JSON clone fails for:
const problematic = {
  date: new Date(),
  regex: /pattern/,
  func: () => "hello",
  undef: undefined,
  map: new Map([["a", 1]])
};

console.log("\nOriginal types:");
console.log("date:", problematic.date.constructor.name);
console.log("regex:", problematic.regex.constructor.name);

const cloned = JSON.parse(JSON.stringify(problematic));
console.log("\nAfter JSON clone:");
console.log("date:", typeof cloned.date); // string, not Date!
console.log("regex:", JSON.stringify(cloned.regex)); // empty object
console.log("func:", cloned.func); // undefined (removed)
console.log("undef:", cloned.undef); // undefined (removed)

// Modern alternative: structuredClone
// const better = structuredClone(problematic);
console.log("\n💡 Use structuredClone() for better deep cloning!");

🏆 Mini-Challenge: A Safe Loader for Untrusted JSON

No blanks now — a brief, an outline and five test cases. This is the single most useful JSON function you will ever write: one that takes input you do not control and always hands back a predictable shape instead of an exception.

The [1,2,3] case is the one most people fail. typeof [] === "object", so a plain type check lets an array straight through, and your code then reads data.name off an array and gets undefined. Use Array.isArray().

JSON Best Practices

✅ DO

❌ DON'T

🎯 Mastery Summary

Practice quiz

Which method converts a JavaScript value into a JSON string?

  • JSON.parse()
  • JSON.encode()
  • JSON.stringify()
  • JSON.toString()

Answer: JSON.stringify(). JSON.stringify() serializes a value to JSON text; JSON.parse() does the reverse.

What does JSON.parse() return?

  • A JavaScript value
  • A JSON string
  • Always an object
  • A Promise

Answer: A JavaScript value. JSON.parse() deserializes JSON text back into a JavaScript value.

What happens to a property whose value is undefined during JSON.stringify()?

  • It becomes null
  • It becomes the string 'undefined'
  • It throws an error
  • It is removed entirely

Answer: It is removed entirely. undefined (and functions) are dropped entirely from objects during serialization.

What do NaN and Infinity become when serialized with JSON.stringify()?

  • 0
  • null
  • Strings
  • They throw

Answer: null. NaN and Infinity cannot be represented in JSON, so they become null.

What happens to a Date when passed to JSON.stringify()?

  • It becomes an ISO string
  • It is removed
  • It stays a Date object
  • It becomes a number

Answer: It becomes an ISO string. A Date is converted to an ISO 8601 string; round-tripping does not restore it to a Date automatically.

What does JSON.stringify(obj) return when obj contains a circular reference?

  • null
  • An empty object
  • It throws a TypeError
  • The string '[Circular]'

Answer: It throws a TypeError. Circular references crash JSON.stringify with a TypeError unless you use a custom replacer.

Which JSON.parse input is valid?

  • {name: 'value'}
  • {"name": "value"}
  • {"a": 1, "b": 2,}
  • {'name': 'value'}

Answer: {"name": "value"}. JSON requires double-quoted keys and strings, and forbids trailing commas.

What is the typeof JSON.stringify({ a: 1 })?

  • object
  • number
  • json
  • string

Answer: string. stringify always produces a string.

What does a replacer ARRAY argument to JSON.stringify do?

  • Renames keys
  • Includes only the listed keys in the output
  • Sorts the keys
  • Removes all values

Answer: Includes only the listed keys in the output. Passing an array of key names whitelists which properties appear in the output.

After const c = JSON.parse(JSON.stringify({ date: new Date() })), what is typeof c.date?

  • object
  • number
  • string
  • undefined

Answer: string. JSON cloning turns the Date into an ISO string, so the clone's date is a string — a key limitation versus structuredClone.

Continue this course