JSON & serialization
JSON has a smaller value model than JavaScript
JSON is a text format. A JSON value can be an object, array, string, number, boolean, or null. JSON has no distinct representation for JavaScript's undefined, functions, BigInts, Dates, Maps, or Sets.
JSON.stringify() converts a supported JavaScript value to JSON text. For some unsupported top-level values it returns undefined, as shown below. JSON.parse() reads JSON text and creates the corresponding JavaScript value.
const text = JSON.stringify({ name: "Ada", active: true });
// '{"name":"Ada","active":true}'
const value = JSON.parse(text);
// { name: "Ada", active: true }
Parsing rejects text outside the JSON grammar with a SyntaxError. JSON object keys and strings use double quotes, and JSON does not allow comments or trailing commas.
Unsupported values do not share one fallback
The location of an unsupported value changes what JSON.stringify() does. An undefined value or an ordinary function without a custom serialization hook is omitted from an object property. The same value in an array becomes null, which retains the array position. Passing undefined or such a function as the top-level value makes JSON.stringify() return undefined instead of a string.
JSON.stringify({ missing: undefined, work() {} });
// '{}'
JSON.stringify([undefined, function work() {}]);
// '[null,null]'
JSON.stringify(undefined);
// undefined
BigInt takes a different path. An encountered BigInt throws a TypeError unless custom serialization, such as a toJSON() method, replaces it first. Encountering a circular reference while recursively serializing also throws a TypeError, because JSON has no representation for object references.
JSON.stringify({ id: 42n });
// TypeError
const node = {};
node.self = node;
JSON.stringify(node);
// TypeError
Wrong "Unsupported JavaScript values all become null."
Right Object properties may disappear, array elements may become null, and top-level values may produce undefined. Unhandled BigInts and encountered cycles throw.
A Date round-trip returns a string
A Date normally inherits Date.prototype.toJSON(). For a valid Date, that standard method returns the same UTC date-time string as toISOString().
const input = { at: new Date("2020-01-02T03:04:05.000Z") };
const text = JSON.stringify(input);
// '{"at":"2020-01-02T03:04:05.000Z"}'
const output = JSON.parse(text);
output.at instanceof Date;
// false
typeof output.at;
// 'string'
Wrong "JSON.parse(JSON.stringify(value)) recreates the original JavaScript types."
Right It recreates values expressible by the JSON text. Type information outside JSON's value model is lost unless the application encodes and restores it explicitly.
The replacer changes what gets written
The second argument to JSON.stringify() can be a function or an array. An array is an inclusion list of property names for non-array objects. A function receives each key and current value. Its return value becomes the value that is serialized.
const account = {
id: 7,
email: "dev@example.com",
passwordHash: "secret",
};
const publicText = JSON.stringify(account, (key, value) => {
if (key === "passwordHash") return undefined;
return value;
});
// '{"id":7,"email":"dev@example.com"}'
JSON.stringify(account, ["id", "email"]);
// '{"id":7,"email":"dev@example.com"}'
Returning undefined, a function, or a symbol from a function replacer omits an object property. An array element still becomes null. The replacer is also called for the root value with "" as its key.
toJSON runs before the replacer
For an object or BigInt with a callable toJSON property, JSON.stringify() calls that method first. It then passes the hook's result to the function replacer. The hook receives the containing property name, an array index as a string, or "" when the value is the root.
const event = {
at: new Date("2020-01-02T03:04:05.000Z"),
};
JSON.stringify(event, (key, value) => {
if (key === "at") {
console.log(typeof value); // 'string'
}
return value;
});
A replacer that looks for value instanceof Date will miss an ordinary Date property because Date.prototype.toJSON() has already returned a string.
Wrong "The replacer always sees the original property value."
Right A callable toJSON() gets the first chance to replace an object or BigInt. The function replacer receives that replacement.
Custom toJSON() methods are useful at a serialization boundary, but parsing does not call an inverse hook. The JSON text must carry enough information for the reader to rebuild a domain type.
The reviver transforms from leaves to root
The second argument to JSON.parse() is a reviver. Parsing happens first. The reviver then visits nested properties before their containing objects, and visits the root last with an empty-string key.
Its return value replaces the current property. Returning undefined, including by reaching the end without return, deletes that property. When this happens for the final root call, JSON.parse() returns undefined. Every unchanged value needs to be returned.
const value = JSON.parse(
'{"createdAt":"2020-01-02T03:04:05.000Z","count":2}',
(key, current) => {
if (key === "createdAt") return new Date(current);
return current;
},
);
value.createdAt instanceof Date;
// true
Matching the known field is safer than converting every ISO-looking string. Once a Date has become JSON text, the parser cannot distinguish a date string from ordinary application text by type alone.
const payload = '{"createdAt":"2020-01-02T03:04:05.000Z","label":"2020-01-02T03:04:05.000Z"}';
const restored = JSON.parse(payload, (key, value) => {
if (key === "createdAt") return new Date(value);
return value;
});
restored.createdAt instanceof Date; // true
typeof restored.label; // 'string'
Wrong "If a reviver does not return anything, JSON.parse keeps the original value."
Right An undefined return deletes the property. Return value on paths that should remain unchanged.
A JSON round-trip is a constrained conversion
This helper is common and narrower than its name suggests:
const deepClone = (value) => JSON.parse(JSON.stringify(value));
It first converts the input to JSON text, applying callable toJSON hooks on encountered objects or BigInts plus the usual omission rules. Parsing can only reconstruct information present in that text. With the standard Date.prototype.toJSON, valid Dates become strings and invalid Dates become null. An unhandled BigInt or a circular reference encountered during recursive serialization makes stringification throw. Without a custom serialization hook, enumerable function-valued object properties disappear and function-valued array elements become null. A top-level function without such a hook makes JSON.stringify() return undefined, so passing that result to JSON.parse() throws a SyntaxError. Map and Set instances have no enumerable own string-keyed data for JSON.stringify() to visit by default, so they become {}.
Repeated references are another loss even when there is no cycle:
const shared = { enabled: true };
const input = { left: shared, right: shared };
const copy = JSON.parse(JSON.stringify(input));
copy.left === copy.right;
// false
JSON has no object-reference notation. Both properties are written as separate objects.
Wrong "If stringify and parse complete, the result is a lossless deep clone."
Right Completion does not imply losslessness. Unsupported properties and type identity may already have been discarded.
What structuredClone preserves
structuredClone() performs the platform's structured clone algorithm and returns a deep copy. It supports circular references and keeps graph relationships. Its supported JavaScript types include Date, Map, Set, BigInt, ArrayBuffer, typed arrays, RegExp, and plain objects.
const shared = { enabled: true };
const input = {
createdAt: new Date("2020-01-02T03:04:05.000Z"),
ids: new Set([1, 2]),
lookup: new Map([[1, "one"]]),
large: 42n,
left: shared,
right: shared,
};
input.self = input;
const copy = structuredClone(input);
copy.createdAt instanceof Date; // true
copy.ids instanceof Set; // true
copy.lookup instanceof Map; // true
copy.large === 42n; // true
copy.left === copy.right; // true
copy.self === copy; // true
This does not make structuredClone() universal. If the algorithm reaches a function as a value to clone, it throws a DataCloneError. The algorithm does not duplicate the prototype chain, property descriptors, getters, or setters. A clone of a custom class should not be assumed to retain that class's prototype or private elements.
class Counter {
increment() {}
}
const copy = structuredClone(new Counter());
copy instanceof Counter;
// false
structuredClone({ run() {} });
// DataCloneError
Wrong "structuredClone copies arbitrary JavaScript objects exactly."
Right It preserves the supported value data and graph structure. Functions and some object metadata fall outside that contract.
Choose by destination
JSON produces interoperable text. Use it when the destination is a JSON API, file, database column, or other text boundary, then define explicit encodings for domain values that JSON cannot carry.
function encodeJob(job) {
return JSON.stringify({
...job,
createdAt: job.createdAt.toISOString(),
retryIds: [...job.retryIds],
});
}
function decodeJob(text) {
const value = JSON.parse(text);
return {
...value,
createdAt: new Date(value.createdAt),
retryIds: new Set(value.retryIds),
};
}
Use structuredClone() for an independent in-memory copy when the whole graph consists of structured-cloneable values. Its optional transfer list can move transferable objects such as an ArrayBuffer; after transfer, the corresponding original resource is unusable.
dig deeper
Primary sources behind these notes - the specs and official docs worth reading in full.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.