JSON

Mental model: JSON is a text format with fewer types than JavaScript, so every round trip loses something.

Level: beginner · about 11 minutes

const user = { name: 'Ada', years: 36, admin: false };

const text = JSON.stringify(user);
console.log(text);              // '{"name":"Ada","years":36,"admin":false}'

const back = JSON.parse(text);
console.log(back.name, typeof text, typeof back); // 'Ada' 'string' 'object'

Object to text, text to object.

JSON is a text format. It knows six things: objects, arrays, strings, numbers, booleans and null. Everything else in JavaScript has to be converted, dropped or represented some other way.

Formatting and filtering: space and replacer

const record = { id: 1, name: 'Ada', password: 'hunter2' };

console.log(JSON.stringify(record, null, 2));           // indented, easy to read

console.log(JSON.stringify(record, ['id', 'name']));    // allow list of keys
// '{"id":1,"name":"Ada"}'

console.log(JSON.stringify(record, (key, value) => (key === 'password' ? undefined : value)));
// returning undefined from the replacer drops the property

Let a value describe itself: toJSON

class Money {
  constructor(amount, currency) { this.amount = amount; this.currency = currency; }
  toJSON() { return `${this.amount} ${this.currency}`; }
}

console.log(JSON.stringify({ price: new Money(9.99, 'GBP') }));
// '{"price":"9.99 GBP"}'

// Date already has toJSON, which is why dates serialise as ISO strings.
console.log(JSON.stringify({ when: new Date(0) })); // '{"when":"1970-01-01T00:00:00.000Z"}'

Rebuilding on the way in: the reviver

const text = '{"name":"Ada","born":"1815-12-10T00:00:00.000Z"}';

const isoDate = /^\d{4}-\d{2}-\d{2}T/;
const parsed = JSON.parse(text, (key, value) =>
  typeof value === 'string' && isoDate.test(value) ? new Date(value) : value
);

console.log(parsed.born instanceof Date);   // true
console.log(parsed.born.getFullYear());     // 1815

What a round trip loses

const value = {
  gone: undefined,
  method() {},
  tag: Symbol('x'),
  big: Infinity,
  nan: NaN,
  set: new Set([1]),
  map: new Map([['a', 1]]),
  when: new Date(0),
};

console.log(JSON.stringify(value));
// '{"big":null,"nan":null,"set":{},"map":{},"when":"1970-01-01T00:00:00.000Z"}'
ValueAfter JSON.stringify
undefined in an objectthe property disappears
undefined in an arraybecomes null
a function or a symbol valuethe property disappears
Infinity, -Infinity, NaNnull
Map, Set{}, all contents lost
Datean ISO string, and it stays a string after parsing
BigIntthrows a TypeError

Circular references throw

const node = { id: 1 };
node.self = node;

try {
  JSON.stringify(node);
} catch (err) {
  console.log(err.name);                     // 'TypeError'
  console.log(err.message.includes('circular')); // true
}

console.log(structuredClone(node).self.id);  // 1, cycles are fine here

Importing JSON as a module

// data/config.json  ->  { "retries": 3 }
import config from './data/config.json' with { type: 'json' };

console.log(config.retries); // 3

// The import attribute is required. Without it the runtime refuses to treat
// the file as JSON, which is a security rule, not a style choice.

Static, cached, and validated at load time. No parsing code needed.

Parsing input you did not write

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

console.log(safeJsonParse('{"ok":true}'));      // { ok: true }
console.log(safeJsonParse('nope', {}));         // {}
console.log(safeJsonParse(localStorageValue(), []));

function localStorageValue() { return undefined; } // pretend it was never set
const out = JSON.stringify({ a: undefined, b: [undefined, 1] });
console.log(out);

undefined as an object property means the property is dropped entirely. Inside an array the position has to be kept, so it becomes null. JSON has no way to write undefined.

Try it yourself

Round trip inspector

const source = {
  id: 1,
  when: new Date(0),
  score: NaN,
  missing: undefined,
  greet() { return 'hi'; },
};

const text = JSON.stringify(source, null, 2);
console.log(text);

const back = JSON.parse(text);
console.log('keys lost:', Object.keys(source).length - Object.keys(back).length);
console.log('when is a Date?', back.when instanceof Date);
console.log('score:', back.score);

Add a Map and a BigInt to source. One serialises to {}, the other throws. Which is worse?

Exercises

Parse without crashing

Write safeJsonParse(text, fallback) which returns the parsed value, or fallback if the text is not valid JSON. The default fallback is null. Valid JSON that parses to null must return null, not the fallback.

Serialise without secrets

Write stringifySafely(value) which returns JSON indented by two spaces, with any property named password, token or secret removed at every depth. Use a replacer function, not a manual copy.

Check yourself

What does this log?
'string' — Date has a toJSON method that produces an ISO string. JSON.parse has no idea that string was ever a date, so you get a string back. Restore it with a reviver or an explicit new Date(...).
How do you drop a property during serialisation?
Return undefined for that key from the replacer function — The replacer runs for every key. Returning undefined omits the property. You can also pass an array of allowed key names as the second argument for a simple allow list.
Why does JSON.stringify throw on some objects?
They contain a circular reference or a BigInt — A cycle cannot be written as finite text, so you get a TypeError mentioning a circular structure. BigInt throws too, because JSON has no representation for it. structuredClone handles cycles if a deep copy is what you actually wanted.

Common mistakes

  • Expecting a Date to survive a round trip. It comes back as a string.
  • Using JSON.parse on stored or network data without a try/catch.
  • Treating a successful parse as validation. Well formed is not the same as correct.

Takeaways

  • JSON has six types. Everything else is converted, dropped, or throws.
  • replacer and toJSON control what goes out, reviver controls what comes back.
  • undefined disappears in objects and becomes null in arrays.
  • Always parse untrusted text inside a try/catch, then validate the shape.