JavaScript JSON

6 minute read

JSON (JavaScript Object Notation) is the lingua franca of data exchange on the web. Every REST API, almost every config file, and most NoSQL databases speak it. JavaScript ships with a built-in JSON object that handles converting between JS values and JSON text. The API is small — parse and stringify — but the corner cases are worth knowing.

What JSON Is

JSON is a text format. Its grammar is a tiny subset of JavaScript object syntax:

{
  "string":  "hello",
  "number":  42,
  "boolean": true,
  "null":    null,
  "array":   [1, 2, 3],
  "object":  { "nested": "ok" }
}

The differences from JS object literals:

  JSON JS
Keys always quoted, double-quoted only quoted or bare
Strings double quotes only single, double, or backtick
Trailing commas ❌ ✅ (in arrays/objects)
Comments ❌ ✅
undefined ❌ ✅
Functions ❌ ✅
NaN, Infinity ❌ — must be null ✅
Date, Map, Set, BigInt ❌ ✅

JSON.stringify — JS → Text

JSON.stringify({ name: "Ada", age: 37 });
// '{"name":"Ada","age":37}'

JSON.stringify([1, 2, 3]);
// '[1,2,3]'

JSON.stringify("hello");        // '"hello"'   (quoted!)
JSON.stringify(42);             // '42'
JSON.stringify(true);           // 'true'
JSON.stringify(null);           // 'null'

Indent for readability with the third argument:

JSON.stringify({ a: 1, b: { c: 2 } }, null, 2);
// {
//   "a": 1,
//   "b": {
//     "c": 2
//   }
// }

The Replacer — Filter or Transform

The second argument is either an array of keys to keep, or a function (key, value) => newValue:

const user = { name: "Ada", age: 37, password: "secret", internalId: "x" };

// Whitelist
JSON.stringify(user, ["name", "age"]);
// '{"name":"Ada","age":37}'

// Function — return undefined to drop
JSON.stringify(user, (key, value) => key === "password" ? undefined : value);
// '{"name":"Ada","age":37,"internalId":"x"}'

Replacer is called for every key as JSON walks the structure (it’s invoked first with ("", rootValue)).

Values JSON Can’t Represent

JSON.stringify(undefined);             // undefined   ← not 'undefined'
JSON.stringify(() => {});              // undefined
JSON.stringify(Symbol("s"));           // undefined

// In an object — those keys are dropped
JSON.stringify({ a: 1, b: undefined, c: () => {} });
// '{"a":1}'

// In an array — they become null
JSON.stringify([1, undefined, () => {}, 4]);
// '[1,null,null,4]'

// NaN and ±Infinity → null
JSON.stringify({ n: NaN, p: Infinity });
// '{"n":null,"p":null}'

// BigInt → ❌ throws
JSON.stringify({ x: 10n });
// TypeError: Do not know how to serialize a BigInt

Warning: JSON.stringify throws on BigInt. If you need BigInts in JSON, convert to string yourself (and parse back manually). It also silently drops functions, undefined, and Symbol properties — sometimes you’ll lose data without an error.

Custom Serialisation with toJSON

If an object has a toJSON() method, stringify calls it and uses the return value:

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

JSON.stringify({ price: new Money(100, "USD") });
// '{"price":"100 USD"}'

Date uses this trick under the hood — that’s why dates serialise to ISO strings:

JSON.stringify({ when: new Date("2026-05-16T10:00:00Z") });
// '{"when":"2026-05-16T10:00:00.000Z"}'

Circular References Throw

const a = { name: "loop" };
a.self = a;
JSON.stringify(a);
// ❌ TypeError: Converting circular structure to JSON

If your data has cycles, you must break them — either with a replacer that tracks visited objects, or by selecting only acyclic fields.

JSON.parse — Text → JS

JSON.parse('{"name":"Ada","age":37}');
// { name: "Ada", age: 37 }

JSON.parse('[1, 2, 3]');     // [1, 2, 3]
JSON.parse('"hello"');       // "hello"
JSON.parse('42');            // 42
JSON.parse('true');          // true
JSON.parse('null');          // null

Invalid JSON throws a SyntaxError:

JSON.parse("{name: 'Ada'}");
// ❌ SyntaxError: Unexpected token n in JSON at position 1

Wrap parse calls in try/catch when the input comes from outside:

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

safeParse("{}");          // {}
safeParse("not json");    // null
safeParse("[1,2]", []);   // [1, 2]

The Reviver — Transform on the Way In

The second argument is a function (key, value) => newValue. Use it to fix up types:

const text = '{"name":"Ada","createdAt":"2026-05-16T10:00:00Z"}';

const user = JSON.parse(text, (key, value) => {
  // Revive ISO date strings into Date objects
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;
});

user.createdAt instanceof Date;   // true

Return undefined to drop the key:

JSON.parse('{"a":1,"b":2,"c":3}', (key, value) =>
  key === "b" ? undefined : value
);
// { a: 1, c: 3 }

Round-Trip Quirks

const obj = {
  date:  new Date(),
  set:   new Set([1, 2]),
  map:   new Map([["k", "v"]]),
  fn:    () => 1,
  und:   undefined,
};

const roundTripped = JSON.parse(JSON.stringify(obj));

roundTripped;
// {
//   date: "2026-06-16T12:34:56.789Z",   ← string, not Date
//   set:  {},                            ← Set has no enumerable own props
//   map:  {},                            ← same
//   // fn and und are missing
// }
Original type After round-trip
Date ISO string
Set, Map {} (empty object)
undefined, function, Symbol dropped (key) or null (array slot)
BigInt ❌ throws on stringify
NaN, Infinity null
Class instance plain object with own enumerable properties only

Tip: JSON.parse(JSON.stringify(x)) is the lazy “deep clone” — works for pure data only. For anything richer, use structuredClone(x) (ES2022) which handles Dates, Maps, Sets, typed arrays, circular references, and more.

Pretty-Printing for Logs

const config = {
  host: "api.example.com",
  port: 443,
  features: ["a", "b", "c"],
};

console.log(JSON.stringify(config, null, 2));
// {
//   "host": "api.example.com",
//   "port": 443,
//   "features": [
//     "a",
//     "b",
//     "c"
//   ]
// }

For tab indentation, pass a tab character: JSON.stringify(config, null, "\t").

A Realistic Example — API Wrapper

async function api(path, opts = {}) {
  const res = await fetch(path, {
    headers: { "Content-Type": "application/json" },
    ...opts,
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });

  if (!res.ok) {
    const errText = await res.text();
    throw new Error(`HTTP ${res.status}: ${errText}`);
  }

  const text = await res.text();
  return text ? JSON.parse(text, dateReviver) : null;
}

function dateReviver(key, value) {
  if (typeof value === "string" &&
      /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value)) {
    return new Date(value);
  }
  return value;
}

// Usage
const user = await api("/users/1");
const created = await api("/users", {
  method: "POST",
  body: { name: "Ada", age: 37 },
});

Stringify outgoing, parse incoming, revive dates, surface errors.

Common Mistakes

// ❌ Parsing already-parsed data
const data = await response.json();   // already an object
const parsed = JSON.parse(data);      // TypeError — data isn't a string

// ❌ Stringifying a Date and expecting a Date back
const t = JSON.parse(JSON.stringify(new Date()));
typeof t;   // "string"

// ❌ Single quotes in JSON
JSON.parse("{'name': 'Ada'}");   // SyntaxError

// ❌ Trailing comma
JSON.parse('[1, 2, 3,]');        // SyntaxError

// ❌ Using JSON for deep cloning rich objects
const original = { date: new Date(), map: new Map() };
const clone = JSON.parse(JSON.stringify(original));
clone.date instanceof Date;      // false
clone.map  instanceof Map;       // false

// ✅
const clone = structuredClone(original);

Best Practices Checklist

  • ✅ Wrap JSON.parse of any external input in try/catch
  • ✅ Use null, 2 (or 4) as stringify args to indent for logs
  • ✅ Use replacer/reviver for type transforms (passwords filtered, dates revived)
  • ✅ Use structuredClone for deep clones of rich objects
  • ✅ Implement toJSON() on classes that need custom serialisation
  • ❌ Don’t trust JSON.parse(JSON.stringify(x)) for non-data types
  • ❌ Don’t try to stringify objects with cycles — break them first
  • ❌ Don’t put undefined, functions, or BigInts where you need them preserved

What’s Next?

Data covered. Now we move from pure-data territory to where JavaScript meets the browser — the DOM. Next chapter walks through selecting, creating, modifying, and removing elements on a page.

← Previous: JS Spread & Rest Next: JS DOM Manipulation →

Updated: