Implement JSON.stringify
JSON.stringify converts a value to JSON text by recursing through it and applying one rule per type. Strings get quoted and escaped, numbers and booleans stringify directly, null becomes "null", arrays and objects recurse. The subtlety is undefined: dropped from objects, but turned into null inside arrays.
The question
Deceptively simple, and that’s the point. Anyone can write the recursion. The interviewer is
watching for whether you know the type rules, and in particular whether you notice that
undefined behaves differently depending on where it appears.
The solution
Returning JavaScript's undefined (rather than the string "undefined")
from the recursive call is what lets the object and array branches apply their different rules.
function jsonStringify(value) {
if (value === null) return "null";
const type = typeof value;
if (type === "number") return Number.isFinite(value) ? String(value) : "null";
if (type === "boolean") return String(value);
if (type === "string") return quote(value);
if (Array.isArray(value)) {
// undefined and functions become null inside an array, not omitted.
const items = value.map((item) => {
const text = jsonStringify(item);
return text === undefined ? "null" : text;
});
return "[" + items.join(",") + "]";
}
if (type === "object") {
const pairs = [];
for (const key of Object.keys(value)) {
const text = jsonStringify(value[key]);
// Keys whose value is undefined are dropped entirely.
if (text !== undefined) pairs.push(quote(key) + ":" + text);
}
return "{" + pairs.join(",") + "}";
}
// undefined, functions and symbols have no JSON representation.
return undefined;
}
function quote(text) {
let out = '"';
for (const char of text) {
if (char === '"') out += '\\"';
else if (char === "\\") out += "\\\\";
else if (char === "\n") out += "\\n";
else if (char === "\r") out += "\\r";
else if (char === "\t") out += "\\t";
else if (char < " ") out += "\\u" + char.charCodeAt(0).toString(16).padStart(4, "0");
else out += char;
}
return out + '"';
}
How it works
One branch per type, with null checked first, because typeof null is
"object" and would otherwise fall through to the object branch.
undefined is the interesting one. The recursive call returns JavaScript’s
undefined, not the string, and each container decides what that means: an object
drops the key, an array writes null. Strings need escaping rather than just
quoting, since a quote or a newline inside one produces JSON that won’t parse back. And
NaN and Infinity become null, which surprises people.
The tests it has to pass
This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 2 cases Practice Pad runs against your solution, and the implementation above passes all of them.
✓ serialises a nested value as JSON text
expectEqual(jsonStringify({ name: "Ada", scores: [3, 5], active: true }), '{"name":"Ada","scores":[3,5],"active":true}');
✓ serialises each primitive on its own
expectEqual(
[jsonStringify("text"), jsonStringify(0), jsonStringify(false), jsonStringify(null)],
['"text"', "0", "false", "null"],
);
Edge cases to watch out for
Treating undefined the same everywhere
JSON.stringify({ a: undefined }) is "{}", but JSON.stringify([undefined]) is "[null]". Returning the string "undefined" from the recursion makes both wrong at once.
Checking typeof before checking null
typeof null === "object", so a null value handled by the object branch produces "{}" instead of "null". Test for null first.
Quoting strings without escaping them
Wrapping a string in quotes isn’t enough. A string containing a quote character or a newline produces invalid JSON that won’t parse back.
Using Object.entries and forgetting insertion order
Not usually a bug, but worth knowing: integer-like keys are enumerated in ascending numeric order before string keys, so { 2: "a", b: "c", 1: "d" } doesn’t stringify in the order it was written.
Follow up questions
Why does the output key order not match what I wrote?
Because integer-like keys are enumerated first, in ascending numeric order, before string keys in insertion order. So { 2: "a", b: "c", 1: "d" } serialises with 1 before 2 before b. It catches people who rely on JSON output being byte-stable for hashing or caching.
What does JSON.stringify do with functions and symbols?
The same as undefined: omitted from objects, converted to null in arrays. None of the three has a JSON representation, so they’re all handled by the same rule.
How does the real JSON.stringify handle circular references?
It throws a TypeError. Detecting that yourself requires tracking the ancestors of the current value, usually with a Set that you add to on the way down and delete from on the way back up. It’s a reasonable follow-up question to volunteer.
What about the replacer and space arguments?
The real signature is JSON.stringify(value, replacer, space). The replacer is a function or an array of keys to keep; space controls indentation. Interviewers rarely ask for either, but knowing they exist is worth a sentence.
Practise this with the tests running
Practice Pad runs these 2 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.
Download for macOS Free. Requires macOS 12 or later on Apple Silicon.