JSON
JSON — JavaScript Object Notation — is the universal data format for the web. It is a small, strict subset of JavaScript literal syntax that any language can parse. The built-in JSON global exposes just two methods: JSON.stringify turns a value into a JSON string, JSON.parse turns a JSON string back into a value. They are simple on the surface, but each has knobs worth knowing.
The basics
const user = { name: "Ada", age: 36, isAdmin: false, tags: ["math", "code"] };
const json = JSON.stringify(user);
console.log(json);
const parsed = JSON.parse(json);
console.log(parsed.name); // 'Ada'{"name":"Ada","age":36,"isAdmin":false,"tags":["math","code"]}Pretty-printing
The third argument of stringify controls indentation. Pass a number for that many spaces, or a string like "\ " for tabs.
console.log(JSON.stringify(user, null, 2));
{
"name": "Ada",
"age": 36,
"isAdmin": false,
"tags": [
"math",
"code"
]
}The replacer — filter or transform on write
The second argument of stringify is a replacer. It can be an array of keys to keep, or a function that visits every key/value pair.
Array replacer — allowlist
const user = { id: 1, name: "Ada", password: "secret" };
console.log(JSON.stringify(user, ["id", "name"]));
// {"id":1,"name":"Ada"}Function replacer — transform
const data = { name: "Ada", password: "secret", visits: 0 };
const safe = JSON.stringify(data, (key, value) => {
if (key === "password") return undefined; // omit
if (typeof value === "number") return value + 1;
return value;
}, 2);
console.log(safe);{
"name": "Ada",
"visits": 1
}The function is called once with an empty key for the root value, then once per property in a depth-first walk. Returning undefined from a function replacer omits that property entirely.
The reviver — transform on read
JSON.parse accepts a reviver as its second argument. It mirrors the replacer: called for every key/value pair, with the chance to return a replacement.
const json = '{"name":"Ada","born":"1815-12-10"}';
const user = JSON.parse(json, (key, value) => {
if (key === "born") return new Date(value);
return value;
});
console.log(user.born instanceof Date); // true
console.log(user.born.getFullYear()); // 1815Gotchas — what JSON cannot represent
JSON is much smaller than JavaScript. Several values either disappear or come back wrong after a stringify/parse round trip.
Functions — omitted entirely. JSON has no concept of functions.
undefined— properties withundefinedvalues are omitted. In arrays they becomenull.Symbols — keyed by symbol? Omitted. Value is a symbol? Omitted.
NaN,Infinity,-Infinity— serialised asnull. Information lost.Dates — serialised by calling
toJSON()(an ISO string). On parse they stay strings unless you revive them.Map,Set, typed arrays — serialised as empty objects{}(no custom handling).BigInt — throws a
TypeErroron stringify.Circular references — throw
TypeError: Converting circular structure to JSON.
const tricky = {
fn: () => 1,
miss: undefined,
notNumber: NaN,
when: new Date("2024-01-01"),
list: [1, undefined, 3],
};
console.log(JSON.stringify(tricky));{"notNumber":null,"when":"2024-01-01T00:00:00.000Z","list":[1,null,3]}toJSON — customise serialisation
If an object has a method called toJSON, JSON.stringify uses its return value instead. Date ships with one; you can add your own.
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
toJSON() {
return { display: `${this.amount} ${this.currency}` };
}
}
console.log(JSON.stringify({ price: new Money(42, "GBP") }));
// {"price":{"display":"42 GBP"}}Parsing safely
JSON.parse throws SyntaxError on invalid input. For user-supplied data, wrap it in try/catch rather than trusting the format.
function safeParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(safeParse('{"ok":true}')); // { ok: true }
console.log(safeParse('not json', {})); // {}