JavaScript JSON
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.parseof any external input intry/catch - ✅ Use
null, 2(or4) asstringifyargs to indent for logs - ✅ Use replacer/reviver for type transforms (passwords filtered, dates revived)
- ✅ Use
structuredClonefor 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.