JavaScript Intermediate #7 Working with JSON and Serialization
The final post in the JavaScript Intermediate series. JSON is the format you almost always pass through when exchanging data with the outside world.
What is JSON? #
JSON (JavaScript Object Notation) is a data interchange format inspired by JavaScript object syntax. It’s a standard supported by nearly every language, not only JavaScript.
{
"id": "u1",
"name": "Curtis",
"age": 30,
"tags": ["dev", "blog"],
"active": true,
"spouse": null
}Similar to a JavaScript object — but with a few differences.
- Keys must be strings wrapped in double quotes
- Values can be only string / number / boolean / null / array / object
- Functions, undefined, Date, Symbol, BigInt are not allowed
- No trailing commas —
{"a": 1,}is invalid
The key takeaway is that not every JavaScript value is representable in JSON.
JSON.parse — string → object
#
const text = '{"id":"u1","name":"Curtis"}';
const obj = JSON.parse(text);
obj.id; // 'u1'
obj.name; // 'Curtis'
Turns a string into a JavaScript object. Throws on bad input.
JSON.parse('{name: "Curtis"}'); // ✗ SyntaxError (unquoted key)
JSON.parse("{'a': 1}"); // ✗ SyntaxError (single quotes)
JSON.parse('{"a": 1,}'); // ✗ SyntaxError (trailing comma)
When parsing external data, you usually wrap with try/catch.
function safeParse(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}JSON.parse’s reviver — transform function
#
You can pass a function as the second argument to transform each key-value.
const text = '{"createdAt": "2026-05-04T10:00:00Z", "name": "Curtis"}';
const obj = JSON.parse(text, (key, value) => {
if (key === 'createdAt' && typeof value === 'string') {
return new Date(value);
}
return value;
});
obj.createdAt instanceof Date; // true
JSON has no Date type, so dates are typically sent as ISO strings. Restoring them to Date objects via a reviver at parse time is a common pattern. In bigger apps, schema libraries like zod do the same job more powerfully.
JSON.stringify — object → string
#
const obj = { id: 'u1', name: 'Curtis' };
JSON.stringify(obj);
// '{"id":"u1","name":"Curtis"}'
Pretty output — indentation #
The third argument controls indentation width. Useful for debugging or saving to a file.
JSON.stringify(obj, null, 2);
// {
// "id": "u1",
// "name": "Curtis"
// }
The null in the second slot is the replacer argument (covered next). Pass null when you don’t need it.
replacer — filter / transform #
The second argument can be an array of keys to include or a transform function.
const user = { id: 'u1', name: 'Curtis', password: 'secret', age: 30 };
JSON.stringify(user, ['id', 'name', 'age']);
// '{"id":"u1","name":"Curtis","age":30}'
// password excluded
JSON.stringify(user, (key, value) => {
if (key === 'password') return undefined; // exclude
return value;
});Returning undefined excludes that key from the output. A useful pattern for masking sensitive info like passwords.
Disappearing values — stringify pitfalls
#
Some JavaScript values can’t be represented in JSON. When stringify meets them, they silently vanish or change.
JSON.stringify({
a: undefined, // key disappears entirely
b: () => {}, // function disappears
c: Symbol('s'), // symbol disappears
d: NaN, // converted to null
e: Infinity, // converted to null
f: -Infinity, // converted to null
});
// '{"d":null,"e":null,"f":null}'
Keys for a, b, c are missing from the output. This sometimes causes bugs — “I’m sure I included that value, but the receiver didn’t get it” is often exactly what’s happening here.
BigInt throws #
JSON.stringify({ count: 100n }); // ✗ TypeError
stringify doesn’t know how to serialize BigInt and throws. You must convert it manually.
const obj = { count: 100n };
JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"count":"100"}'
toJSON method — class self-serialization
#
If an object has a toJSON method, stringify serializes that method’s return value instead.
class User {
constructor(name, password) {
this.name = name;
this._password = password;
}
toJSON() {
return { name: this.name }; // exclude password
}
}
const u = new User('Curtis', 'secret');
JSON.stringify(u);
// '{"name":"Curtis"}'
Built-in Date uses exactly this pattern.
const d = new Date('2026-05-04T10:00:00Z');
d.toJSON(); // '2026-05-04T10:00:00.000Z'
JSON.stringify({ d }); // '{"d":"2026-05-04T10:00:00.000Z"}'
That’s why Date is auto-converted to an ISO string.
Circular references — stringify throws #
const a = { name: 'Curtis' };
a.self = a;
JSON.stringify(a); // ✗ TypeError: Converting circular structure to JSON
When an object refers to itself, stringify throws to avoid an infinite loop. Comes up serializing big graphs (trees with parent references).
Solution: break the cycle or handle via replacer.
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
});
}structuredClone — modern replacement for the JSON trick
#
In the past, you’d often do this for deep copy.
const copy = JSON.parse(JSON.stringify(original));It couldn’t handle functions, undefined, Date, Map, Set. Date became an ISO string; functions/undefined disappeared.
Since ES2022, the structuredClone we saw in #4 is standardized.
const original = {
name: 'Curtis',
createdAt: new Date(),
tags: new Set(['a', 'b']),
};
const copy = structuredClone(original);
copy.createdAt instanceof Date; // true
copy.tags instanceof Set; // true
Most data structures are copied exactly. The limitation is that functions and class-instance prototypes can’t be carried over, but plain objects, arrays, Date, Map, and Set are all fine. In new code, structuredClone is the answer for deep copy.
Common mini-recipes #
1) Safe deep copy with fallback #
function deepCopy(value) {
if (typeof structuredClone === 'function') {
return structuredClone(value);
}
return JSON.parse(JSON.stringify(value));
}2) URL-safe encode/decode #
btoa / atob are base64 helpers. For safely keeping a small object in a URL/storage.
const data = { id: 'u1', name: 'Curtis' };
const encoded = btoa(JSON.stringify(data));
// "eyJpZCI6InUxIiwibmFtZSI6IkN1cnRpcyJ9"
const decoded = JSON.parse(atob(encoded));
// { id: 'u1', name: 'Curtis' }
3) Validate then parse #
Schema libraries like zod are standard in big apps, but for lighter cases a hand-written check is shorter.
function parseUser(text) {
const obj = JSON.parse(text);
if (typeof obj?.id !== 'string') throw new Error('id missing');
if (typeof obj?.name !== 'string') throw new Error('name missing');
return obj;
}TypeScript + React Practice #6 covers solving the same problem more powerfully with zod.
Wrap-up #
What we covered:
- JSON allows only string/number/boolean/null/array/object, with double-quoted keys
- Restore types like Date with
JSON.parse’s reviver - Pretty-print with
JSON.stringify’s third argument - Use replacer to filter/transform keys
undefined/functions/Symbol disappear in stringify- BigInt throws — handle yourself
- A
toJSONmethod lets stringify use its result (how Date works) - Circular references throw — track with WeakSet
structuredCloneis the modern answer for deep copy
Closing the Intermediate series #
What we covered across 7 posts:
- Classes —
#/static/get/set(#1) - Async — Promise, async/await, Promise.all (#2)
- Iterators/generators —
Symbol.iterator,function*,yield(#3) - Destructuring/spread/rest in depth — parameter patterns, immutable update (#4)
?.and??— safe access and nullish defaults (#5)- fetch API — standard network tool, AbortController (#6)
- Working with JSON — parse/stringify, structuredClone (this post)
With these in hand, almost every everyday modern-JavaScript expression is within reach. The next Advanced series goes one step deeper — closures, this, prototypes, the event loop, the memory model — covering how the JavaScript engine actually works.