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"}'
| Value | After JSON.stringify |
|---|---|
undefined in an object | the property disappears |
undefined in an array | becomes null |
| a function or a symbol value | the property disappears |
Infinity, -Infinity, NaN | null |
Map, Set | {}, all contents lost |
Date | an ISO string, and it stays a string after parsing |
BigInt | throws 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' —
Datehas atoJSONmethod that produces an ISO string.JSON.parsehas no idea that string was ever a date, so you get a string back. Restore it with a reviver or an explicitnew Date(...). - How do you drop a property during serialisation?
- Return
undefinedfor that key from the replacer function — The replacer runs for every key. Returningundefinedomits the property. You can also pass an array of allowed key names as the second argument for a simple allow list. - Why does
JSON.stringifythrow on some objects? - They contain a circular reference or a
BigInt— A cycle cannot be written as finite text, so you get aTypeErrormentioning a circular structure.BigIntthrows too, because JSON has no representation for it.structuredClonehandles cycles if a deep copy is what you actually wanted.
Common mistakes
- Expecting a
Dateto survive a round trip. It comes back as a string. - Using
JSON.parseon stored or network data without atry/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.
replacerandtoJSONcontrol what goes out,revivercontrols what comes back.undefineddisappears in objects and becomesnullin arrays.- Always parse untrusted text inside a
try/catch, then validate the shape.