JSON

Move data in and out of JSON without losing anything you needed.

13of 32
≈ 9Min read
8Parts

What JSON is

JSON is a text format, not a JavaScript type. It looks like an object literal and is deliberately much smaller: six value types, double quotes only, no trailing commas, no comments, no functions.

the whole grammar
{
  "name": "Painted",        // keys must be double-quoted strings
  "days": 5,
  "archived": false,
  "note": null,
  "tags": ["flush", "1mm"],
  "spec": { "tolerance": 1 }
}

// not allowed: single quotes, trailing commas, comments,
// undefined, NaN, Infinity, functions, Dates, Maps, Sets

stringify and parse

both directions
JSON.stringify(door);              // '{"name":"Painted","days":5}'
JSON.stringify(door, null, 2);     // indented, for a file or a log
JSON.parse(text);                  // back to a value

// the second argument picks or transforms keys
JSON.stringify(door, ["name", "days"]);
JSON.stringify(door, (k, v) => (k === "secret" ? undefined : v));

What does not survive the round trip

the Date one catches everyone
const saved = JSON.parse(JSON.stringify({ at: new Date() }));
typeof saved.at;          // "string", not an object
saved.at.getFullYear();   // TypeError
ValueAfter stringify → parse
undefined in an objectThe key disappears entirely
undefined in an arrayBecomes null
A functionDisappears (or becomes null in an array)
NaN, Infinitynull
A DateAn ISO string
A Map or Set{} — an empty object
A BigIntThrows a TypeError
-00

Reviving

A reviver runs for every key from the inside out. It is the clean place to turn date strings back into Date objects, or to validate as you parse.

the second argument to parse
const ISO = /^\d{4}-\d{2}-\d{2}T/;

const data = JSON.parse(text, (key, value) =>
  typeof value === "string" && ISO.test(value) ? new Date(value) : value
);

toJSON

Date already has a toJSON, which is why a date becomes an ISO string rather than an empty object.

an object can decide how it serialises
class Money {
  constructor(minor) { this.minor = minor; }
  toJSON() { return { amount: this.minor / 100, currency: "AZN" }; }
}

JSON.stringify({ total: new Money(1234) });
// '{"total":{"amount":12.34,"currency":"AZN"}}'

Parsing can throw

always, always wrap it
function safeParse(text, fallback = null) {
  try {
    return JSON.parse(text);
  } catch {
    return fallback;
  }
}

// the most common source of a crash: a truncated or empty response
safeParse(localStorage.getItem("prefs") ?? "null", {});
JSON.parse(null) does not throw

It returns null, because String(null) is "null", which is valid JSON. JSON.parse("") does throw. Handle both.

Over the network

res.json() throws if the body is not JSON — which happens when a server returns an HTML error page with a 200. If a response can be either, read res.text() and parse it yourself.

the shape of a real call
const res = await fetch("/api/doors", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(door),
});

if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const data = await res.json();      // parses for you, and throws on bad JSON

Keys have no order guarantee

JSON.stringify writes keys in the object's own insertion order, so two objects with the same data but different key order produce different strings. That makes JSON.stringify(a) === JSON.stringify(b) a fragile equality test.

If you need a stable string — for a cache key, or a checksum — sort the keys first.

a stable stringify
const stable = (o) =>
  JSON.stringify(o, Object.keys(o).sort());

Practice