Back to JavaScript
JavaScript
easy
mid

How would you implement a polyfill for JSON.stringify?

Recursively serialize by type: primitives (string→quoted, number/boolean→String, null→'null'), arrays → [...], objects → {...} with quoted keys. Skip undefined/functions/symbols in objects (→ omitted) but render them as null in arrays. Handle the gotchas: NaN/Infinity→null, toJSON(), circular refs throw.

5 min read·~18 min to think through

A JSON.stringify polyfill is a recursive type-dispatch problem. The interview is about knowing JSON's many special cases.

The core structure

js
function jsonStringify(value) {
  // toJSON hook — Date and custom objects use it
  if (value && typeof value.toJSON === "function") {
    value = value.toJSON();
  }

  const type = typeof value;

  // primitives
  if (value === null) return "null";
  if (type === "number") return Number.isFinite(value) ? String(value) : "null"; // NaN/Infinity → null
  if (type === "boolean") return String(value);
  if (type === "string") return quote(value);                                    // escape + wrap in ""
  if (type === "undefined" || type === "function" || type === "symbol") {
    return undefined; // signal "omit me" — handled by the caller
  }

  // arrays
  if (Array.isArray(value)) {
    const items = value.map((item) => {
      const s = jsonStringify(item);
      return s === undefined ? "null" : s;     // undefined/fn/symbol in arrays → null
    });
    return "[" + items.join(",") + "]";
  }

  // plain objects
  if (type === "object") {
    const pairs = [];
    for (const key of Object.keys(value)) {
      const s = jsonStringify(value[key]);
      if (s !== undefined) {                   // undefined/fn/symbol values → key OMITTED
        pairs.push(quote(key) + ":" + s);
      }
    }
    return "{" + pairs.join(",") + "}";
  }
}

function quote(str) {
  // minimal: escape \, ", control chars; wrap in double quotes
  return '"' + str.replace(/[\\"]/g, "\\$&").replace(/\n/g, "\\n") + '"';
}

The gotchas being tested

  1. undefined, functions, symbolsomitted as object values (the key disappears entirely), but become null as array elements. Different behavior by context.
  2. NaN / Infinity / -Infinity → serialized as "null".
  3. null"null" (string).
  4. toJSON() — if a value has a toJSON method, call it and serialize the result. This is how Date becomes an ISO string.
  5. Strings need escaping — quotes, backslashes, newlines, control characters, then wrapped in double quotes.
  6. Circular references → the real JSON.stringify throws a TypeError. A complete polyfill tracks a visited Set/WeakSet and throws on a cycle.
  7. Object keys are always double-quoted strings.
  8. BigInt → throws a TypeError.
  9. (Full spec also: the replacer and space arguments — mention them; usually out of scope for the core exercise.)

The framing

"It's recursive type dispatch. Primitives: strings get escaped and quoted, finite numbers and booleans stringify, null is 'null', and NaN/Infinity become 'null'. Arrays recurse and join in brackets. Objects recurse over Object.keys with quoted keys. The gotchas are the point: undefined/functions/symbols are omitted as object values but become null in arrays; toJSON is called if present — that's how Dates serialize; strings need proper escaping; and circular references throw a TypeError, so a complete version tracks a visited set."

Follow-up questions

  • Why does undefined behave differently in an object vs an array?
  • How does Date end up as a string — what hook is used?
  • What does JSON.stringify do with a circular reference?
  • What happens to NaN, Infinity, and BigInt?

Common mistakes

  • Treating undefined/function values the same in objects and arrays.
  • Forgetting NaN/Infinity serialize to null.
  • Not escaping special characters in strings.
  • Not handling toJSON (so Dates break).
  • Not detecting circular references — infinite recursion / stack overflow.

Performance considerations

  • O(n) over the value graph. String concatenation/joining and the visited-set lookups for cycle detection are the costs; for huge objects, building an array of parts and joining once is better than repeated concatenation.

Edge cases

  • Circular references → must throw TypeError.
  • NaN, Infinity, -Infinity → null.
  • undefined/function/symbol — omitted in objects, null in arrays.
  • Date (via toJSON), and objects with custom toJSON.
  • BigInt → throws.
  • Strings with quotes, backslashes, newlines, control characters.

Real-world examples

  • Understanding why a Date round-trips to an ISO string but not back to a Date.
  • Debugging why a function or undefined silently disappeared from serialized output.

Senior engineer discussion

Seniors structure it as recursive type dispatch and demonstrate command of the special cases — context-dependent undefined, NaN/Infinity→null, toJSON, string escaping, and circular-reference detection that throws — rather than just the happy path.

Related questions