JSON: objects that travel
An object lives in your program's memory. To send it to a server, save it in a file, or put it in local storage, it has to become text — because text is the only thing that survives the journey.
JSON is that text format, and JavaScript has exactly two functions for it.
const student = { name: "Ayesha", marks: 72 };
const text = JSON.stringify(student);
console.log(text);
console.log(typeof text);
{"name":"Ayesha","marks":72}
stringThat looks almost identical to the object and is a completely different kind of
thing. It is a string. You cannot read text.name from it.
stringify goes out, parse comes back
const student = { name: "Ayesha", marks: 72 };
const text = JSON.stringify(student);
const back = JSON.parse(text);
console.log(back.name);
console.log(back.marks + 1);
console.log(typeof back);
Ayesha 73 object
JSON.parse turns the text back into a real object with working properties.
Those two functions are the entire API, and almost every mistake with JSON is
forgetting which side of them you are on.
const text = '{"name":"Ayesha"}';
console.log(text.name);
console.log(JSON.parse(text).name);
undefined Ayesha
text.name is undefined because a string has no name property. Whenever a
property reads as undefined on data that arrived from somewhere, check
whether you parsed it.
JSON is stricter than JavaScript
JSON looks like JavaScript object syntax and is a narrower language.
console.log(JSON.stringify({ name: "Ayesha", ok: true, note: null }));
{"name":"Ayesha","ok":true,"note":null}The rules:
- Keys must be double-quoted strings.
{name: "x"}is valid JavaScript and invalid JSON. - Strings use double quotes only. Single quotes are not allowed.
- No trailing commas. Legal in modern JavaScript, never in JSON.
- No comments. There is no way to write one.
- Values may be a string, number, boolean,
null, array or object. Nothing else.
try {
JSON.parse("{'name': 'Ayesha'}");
} catch (error) {
console.log(error.name);
}
SyntaxError
Single quotes, so it is not JSON. JSON.parse throws rather than guessing,
which is the right behaviour and the reason for the section below on catching
it.
What stringify silently drops
This is the part that produces confusing bugs, because nothing warns you.
const record = {
name: "Ayesha",
greet() { return "hi"; },
missing: undefined,
when: new Date("2024-03-01T10:00:00Z"),
big: 5,
};
console.log(JSON.stringify(record));
{"name":"Ayesha","when":"2024-03-01T10:00:00.000Z","big":5}The function is gone. The undefined property is gone — not set to null,
gone, the key is absent entirely. And the Date has quietly become a
string, so after a round trip it is no longer a date:
const back = JSON.parse(JSON.stringify({ when: new Date("2024-03-01") }));
console.log(typeof back.when);
console.log(back.when instanceof Date);
string false
Calling back.when.getFullYear() throws. If dates matter, convert them back
explicitly after parsing — new Date(back.when).
In arrays the treatment differs again: undefined becomes null rather than
disappearing, because dropping it would shift every later index.
console.log(JSON.stringify([1, undefined, 3]));
console.log(JSON.stringify({ a: 1, b: undefined, c: 3 }));
[1,null,3]
{"a":1,"c":3}Same value, two different fates, decided by whether it sat in an array or an object.
Formatting for humans
JSON.stringify takes two more arguments. The third is indentation.
const student = { name: "Ayesha", marks: { maths: 72 } };
console.log(JSON.stringify(student, null, 2));
{
"name": "Ayesha",
"marks": {
"maths": 72
}
}That is the standard way to write a config file or print something readable
while debugging. The null in the middle is the second argument, which can
filter properties:
const student = { name: "Ayesha", marks: 72, secret: "x" };
console.log(JSON.stringify(student, ["name", "marks"]));
{"name":"Ayesha","marks":72}Passing an array of keys keeps only those — a quick way to avoid sending fields that should not leave the server.
Nesting works, cycles do not
const order = {
customer: { name: "Bilal" },
items: [{ name: "chai", qty: 2 }],
};
const back = JSON.parse(JSON.stringify(order));
console.log(back.customer.name);
console.log(back.items[0].qty);
Bilal 2
Any depth of objects and arrays round-trips fine. What does not is an object that refers back to itself:
const a = { name: "a" };
a.self = a;
try {
JSON.stringify(a);
} catch (error) {
console.log(error.name);
}
TypeError
TypeError: Converting circular structure to JSON. It cannot be represented as
text, so there is nothing sensible to produce. It usually means you attached a
parent reference somewhere and can drop it before stringifying.
Always catch parse
Data from a server, a file or local storage is not guaranteed to be valid JSON. A failed parse throws, and an unhandled throw stops everything.
function safeParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(safeParse('{"name":"Ayesha"}'));
console.log(safeParse("not json"));
console.log(safeParse("not json", {}));
{ name: 'Ayesha' }
null
{}catch with no binding is allowed when you do not need the error object.
The commonest real cause of a parse failure is not corrupt data — it is an
error page. A server that returns HTML saying "500 Internal Server Error" makes
JSON.parse fail on the very first character, which is why the message so
often reads Unexpected token '<'. That < is the start of <!DOCTYPE html>,
and the actual problem is on the server.
The deep-copy trick, and why not to use it
const original = { name: "Ayesha", marks: { maths: 72 } };
const copy = JSON.parse(JSON.stringify(original));
copy.marks.maths = 100;
console.log(original.marks.maths);
72
It works — the nested object really is independent, unlike a spread copy. But
it silently destroys everything listed above: dates become strings, functions
and undefined vanish, and it throws on a cycle.
structuredClone(original) does the same job properly and is built in. Use
that.
Storing and sending
const settings = { theme: "dark", fontSize: 16 };
const stored = JSON.stringify(settings);
const loaded = JSON.parse(stored);
console.log(loaded.theme);
dark
localStorage holds strings only, so this stringify-and-parse pair is exactly
what you write around it. When fetching from a server, response.json() does
the parsing for you — it is JSON.parse with the network attached, and it
throws in the same way when the response is not JSON.
The two rules
- JSON is text. Parse it before reading properties; stringify before sending or storing.
- Assume the parse can fail, because the failure is usually somebody else's server having a bad day.
That closes the arrays and objects section. If a property is coming back
undefined, the objects lesson covers the access rules,
and destructuring covers pulling the
parsed data apart.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes