JavaScript Intermediate #7 Working with JSON and Serialization

6 min read

The final post in the JavaScript Intermediate series. JSON is the format you almost always pass through when exchanging data with the outside world.

What is JSON? #

JSON (JavaScript Object Notation) is a data interchange format inspired by JavaScript object syntax. It’s a standard supported by nearly every language, not only JavaScript.

JSON example
{
  "id": "u1",
  "name": "Curtis",
  "age": 30,
  "tags": ["dev", "blog"],
  "active": true,
  "spouse": null
}

Similar to a JavaScript object — but with a few differences.

  • Keys must be strings wrapped in double quotes
  • Values can be only string / number / boolean / null / array / object
  • Functions, undefined, Date, Symbol, BigInt are not allowed
  • No trailing commas — {"a": 1,} is invalid

The key takeaway is that not every JavaScript value is representable in JSON.

JSON.parse — string → object #

basic parse
const text = '{"id":"u1","name":"Curtis"}';
const obj = JSON.parse(text);

obj.id;     // 'u1'
obj.name;   // 'Curtis'

Turns a string into a JavaScript object. Throws on bad input.

invalid JSON
JSON.parse('{name: "Curtis"}');   // ✗ SyntaxError (unquoted key)
JSON.parse("{'a': 1}");           // ✗ SyntaxError (single quotes)
JSON.parse('{"a": 1,}');          // ✗ SyntaxError (trailing comma)

When parsing external data, you usually wrap with try/catch.

safe parse
function safeParse(text) {
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

JSON.parse’s reviver — transform function #

You can pass a function as the second argument to transform each key-value.

reviver — restore Date
const text = '{"createdAt": "2026-05-04T10:00:00Z", "name": "Curtis"}';

const obj = JSON.parse(text, (key, value) => {
  if (key === 'createdAt' && typeof value === 'string') {
    return new Date(value);
  }
  return value;
});

obj.createdAt instanceof Date;   // true

JSON has no Date type, so dates are typically sent as ISO strings. Restoring them to Date objects via a reviver at parse time is a common pattern. In bigger apps, schema libraries like zod do the same job more powerfully.

JSON.stringify — object → string #

basic stringify
const obj = { id: 'u1', name: 'Curtis' };
JSON.stringify(obj);
// '{"id":"u1","name":"Curtis"}'

Pretty output — indentation #

The third argument controls indentation width. Useful for debugging or saving to a file.

indentation
JSON.stringify(obj, null, 2);
// {
//   "id": "u1",
//   "name": "Curtis"
// }

The null in the second slot is the replacer argument (covered next). Pass null when you don’t need it.

replacer — filter / transform #

The second argument can be an array of keys to include or a transform function.

serialize only specific keys
const user = { id: 'u1', name: 'Curtis', password: 'secret', age: 30 };

JSON.stringify(user, ['id', 'name', 'age']);
// '{"id":"u1","name":"Curtis","age":30}'
// password excluded
dynamic transform via function
JSON.stringify(user, (key, value) => {
  if (key === 'password') return undefined;   // exclude
  return value;
});

Returning undefined excludes that key from the output. A useful pattern for masking sensitive info like passwords.

Disappearing values — stringify pitfalls #

Some JavaScript values can’t be represented in JSON. When stringify meets them, they silently vanish or change.

stringify pitfall
JSON.stringify({
  a: undefined,         // key disappears entirely
  b: () => {},           // function disappears
  c: Symbol('s'),        // symbol disappears
  d: NaN,                // converted to null
  e: Infinity,           // converted to null
  f: -Infinity,          // converted to null
});
// '{"d":null,"e":null,"f":null}'

Keys for a, b, c are missing from the output. This sometimes causes bugs — “I’m sure I included that value, but the receiver didn’t get it” is often exactly what’s happening here.

BigInt throws #

BigInt
JSON.stringify({ count: 100n });   // ✗ TypeError

stringify doesn’t know how to serialize BigInt and throws. You must convert it manually.

BigInt serialization
const obj = { count: 100n };

JSON.stringify(obj, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"count":"100"}'

toJSON method — class self-serialization #

If an object has a toJSON method, stringify serializes that method’s return value instead.

custom via toJSON
class User {
  constructor(name, password) {
    this.name = name;
    this._password = password;
  }
  toJSON() {
    return { name: this.name };   // exclude password
  }
}

const u = new User('Curtis', 'secret');
JSON.stringify(u);
// '{"name":"Curtis"}'

Built-in Date uses exactly this pattern.

Date's toJSON
const d = new Date('2026-05-04T10:00:00Z');
d.toJSON();              // '2026-05-04T10:00:00.000Z'
JSON.stringify({ d });   // '{"d":"2026-05-04T10:00:00.000Z"}'

That’s why Date is auto-converted to an ISO string.

Circular references — stringify throws #

circular reference pitfall
const a = { name: 'Curtis' };
a.self = a;

JSON.stringify(a);   // ✗ TypeError: Converting circular structure to JSON

When an object refers to itself, stringify throws to avoid an infinite loop. Comes up serializing big graphs (trees with parent references).

Solution: break the cycle or handle via replacer.

track visited with WeakSet
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

structuredClone — modern replacement for the JSON trick #

In the past, you’d often do this for deep copy.

old deep-copy trick — limited
const copy = JSON.parse(JSON.stringify(original));

It couldn’t handle functions, undefined, Date, Map, Set. Date became an ISO string; functions/undefined disappeared.

Since ES2022, the structuredClone we saw in #4 is standardized.

structuredClone — built-in
const original = {
  name: 'Curtis',
  createdAt: new Date(),
  tags: new Set(['a', 'b']),
};

const copy = structuredClone(original);
copy.createdAt instanceof Date;   // true
copy.tags instanceof Set;          // true

Most data structures are copied exactly. The limitation is that functions and class-instance prototypes can’t be carried over, but plain objects, arrays, Date, Map, and Set are all fine. In new code, structuredClone is the answer for deep copy.

Common mini-recipes #

1) Safe deep copy with fallback #

safe copy
function deepCopy(value) {
  if (typeof structuredClone === 'function') {
    return structuredClone(value);
  }
  return JSON.parse(JSON.stringify(value));
}

2) URL-safe encode/decode #

btoa / atob are base64 helpers. For safely keeping a small object in a URL/storage.

object → base64
const data = { id: 'u1', name: 'Curtis' };

const encoded = btoa(JSON.stringify(data));
// "eyJpZCI6InUxIiwibmFtZSI6IkN1cnRpcyJ9"

const decoded = JSON.parse(atob(encoded));
// { id: 'u1', name: 'Curtis' }

3) Validate then parse #

Schema libraries like zod are standard in big apps, but for lighter cases a hand-written check is shorter.

simple validation
function parseUser(text) {
  const obj = JSON.parse(text);
  if (typeof obj?.id !== 'string') throw new Error('id missing');
  if (typeof obj?.name !== 'string') throw new Error('name missing');
  return obj;
}

TypeScript + React Practice #6 covers solving the same problem more powerfully with zod.

Wrap-up #

What we covered:

  • JSON allows only string/number/boolean/null/array/object, with double-quoted keys
  • Restore types like Date with JSON.parse’s reviver
  • Pretty-print with JSON.stringify’s third argument
  • Use replacer to filter/transform keys
  • undefined/functions/Symbol disappear in stringify
  • BigInt throws — handle yourself
  • A toJSON method lets stringify use its result (how Date works)
  • Circular references throw — track with WeakSet
  • structuredClone is the modern answer for deep copy

Closing the Intermediate series #

What we covered across 7 posts:

  1. Classes#/static/get/set (#1)
  2. Async — Promise, async/await, Promise.all (#2)
  3. Iterators/generatorsSymbol.iterator, function*, yield (#3)
  4. Destructuring/spread/rest in depth — parameter patterns, immutable update (#4)
  5. ?. and ?? — safe access and nullish defaults (#5)
  6. fetch API — standard network tool, AbortController (#6)
  7. Working with JSON — parse/stringify, structuredClone (this post)

With these in hand, almost every everyday modern-JavaScript expression is within reach. The next Advanced series goes one step deeper — closures, this, prototypes, the event loop, the memory model — covering how the JavaScript engine actually works.

X