ESC

Type to search the knowledge base.

JSON parse and stringify pitfalls

JSON.stringify/parse footguns — undefined, dates, NaN, cycles, toJSON, revivers, and safe parsing of untrusted input.

beginner3 min read
  • javascript
  • json
  • parse
  • stringify

JSON is the lingua franca of APIs and localStorage. It is not a full clone of JavaScript values. Most “mystery data loss” bugs are stringify dropping fields you assumed would survive.

What survives

JSON.stringify({
  n: 1,
  s: 'ok',
  b: true,
  arr: [1, null, 'x'],
  nested: { a: 1 },
});

Numbers, strings, booleans, null, arrays, plain objects. Keys become strings.

What dies or mutates

JSON.stringify({
  u: undefined, // key omitted
  fn: () => {}, // key omitted
  sym: Symbol('x'), // key omitted
  nan: NaN, // null
  inf: Infinity, // null
  date: new Date(), // ISO string, not Date
  map: new Map([['a', 1]]), // {}
  set: new Set([1]), // {}
});

JSON.stringify([undefined, () => {}]); // "[null,null]"
const a = {};
a.self = a;
JSON.stringify(a); // TypeError: cyclic structure

toJSON

const money = {
  cents: 199,
  toJSON() {
    return { usd: (this.cents / 100).toFixed(2) };
  },
};
JSON.stringify(money); // '{"usd":"1.99"}'

Date.prototype.toJSON explains the ISO string conversion.

replacer and space

JSON.stringify(obj, ['id', 'name']); // whitelist keys
JSON.stringify(obj, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value,
);
JSON.stringify(obj, null, 2); // pretty print

parse and reviver

JSON.parse('{"a":1}'); // { a: 1 }
JSON.parse('x'); // SyntaxError

const data = JSON.parse('{"when":"2026-08-04T00:00:00.000Z"}', (key, value) => {
  if (key === 'when') return new Date(value);
  return value;
});

Safe parse helper

function safeJsonParse(text, fallback = null) {
  try {
    return JSON.parse(text);
  } catch {
    return fallback;
  }
}

// localStorage
const prefs = safeJsonParse(localStorage.getItem('prefs'), {});

Never JSON.parse without handling SyntaxError for external/storage input.

Security

JSON.parse does not execute code (unlike eval). Still:

  • Validate shape after parse (schema, zod, manual checks)
  • Don’t assign parsed HTML to innerHTML
  • Prototype pollution: careful merges of objects with __proto__ keys from untrusted JSON (use Object.create(null) or null-prototype merges)
const raw = JSON.parse('{"__proto__":{"polluted":true}}');
// merging into {} with broken libs can pollute — use safe merge

Deep clone myth

const clone = JSON.parse(JSON.stringify(obj)); // lossy!

Prefer structuredClone for clones; JSON only for JSON-shaped data.

Interview answer

“JSON supports a subset of JS values. stringify drops undefined/functions/symbols, turns NaN/Infinity into null, Dates into strings, and throws on cycles. parse throws SyntaxError on bad input—always try/catch for external data. I use reviver/replacer carefully and never treat JSON as a general deep clone.”

Pretty print and stable keys

// deterministic stringify for hashing/cache keys (simple version)
function stableStringify(value) {
  if (value && typeof value === 'object' && !Array.isArray(value)) {
    const sorted = Object.keys(value)
      .sort()
      .reduce((acc, k) => {
        acc[k] = value[k];
        return acc;
      }, {});
    return JSON.stringify(sorted, (_, v) =>
      typeof v === 'object' && v && !Array.isArray(v)
        ? JSON.parse(stableStringify(v))
        : v,
    );
  }
  return JSON.stringify(value);
}

JSON object key order is insertion order in modern engines, but producers differ — stabilize when the string is used as a cache key. For logs, JSON.stringify(err) remains useless; pick fields. For config files, prefer parsers that allow comments only if you control the format — standard JSON.parse rejects comments.

Further reading

Related guides