Learn the concept
Objects & Copying
A JSON.stringify polyfill recursively converts JavaScript values to JSON strings by handling each type differently: strings get quoted, numbers and booleans become literals, arrays and objects are recursively stringified, and special values like undefined, functions, and Symbols are either omitted or converted to null.
Implementing JSON.stringify from scratch is a common interview question at companies like Swiggy. It tests understanding of JavaScript type system, recursion, and edge case handling.
JSON.stringify(value, replacer?, space?) converts a value to a JSON string:
Type Handling Rules:
| JavaScript Type | JSON Output |
|---|---|
| string | Quoted string with escaped special chars |
| number (finite) | Number literal |
| NaN, Infinity | null |
| boolean | true or false |
| null | null |
| undefined | Omitted in objects, null in arrays |
| function | Omitted in objects, null in arrays |
| Symbol | Omitted in objects, null in arrays |
| BigInt | Throws TypeError |
| Date | Calls .toISOString() (via toJSON) |
| Array | [...] with elements stringified |
| Object | {...} with enumerable own properties |
Special Behaviors:
toJSON() method: call it and stringify the return valuereplacer parameter can be a function (transforms values) or an array (whitelist of keys)space parameter controls indentation (number of spaces or a string prefix)toJSON() methodfunction myStringify(value, replacer, space) {
const seen = new WeakSet(); // Circular reference detection
function stringify(val, key, holder) {
// Apply replacer function
if (typeof replacer === 'function') {
val = replacer.call(holder, key, val);
}
// Handle toJSON method
if (val !== null && typeof val === 'object' && typeof val.toJSON === 'function') {
val = val.toJSON(key);
}
// Primitives
if (val === null) return 'null';
if (val === true) return 'true';
if (val === false) return 'false';
if (typeof val === 'string') return escapeString(val);
if (typeof val === 'number') {
return Number.isFinite(val) ? String(val) : 'null';
}
if (typeof val === 'bigint') {
throw new TypeError('BigInt value can\'t be serialized in JSON');
}
// undefined, functions, and Symbols return undefined
if (typeof val === 'undefined' || typeof val === 'function' || typeof val === 'symbol') {
return undefined;
}
// Objects and arrays
if (typeof val === 'object') {
// Circular reference check
if (seen.has(val)) {
throw new TypeError('Converting circular structure to JSON');
}
seen.add(val);
let result;
if (Array.isArray(val)) {
result = stringifyArray(val);
} else {
result = stringifyObject(val);
}
seen.delete(val);
return result;
}
return undefined;
}
function stringifyArray(arr) {
const items = arr.map((item, i) => {
const val = stringify(item, String(i), arr);
return val === undefined ? 'null' : val;
});
return `[${items.join(',')}]`;
}
function stringifyObject(obj) {
const keys = typeof replacer === 'object' && Array.isArray(replacer)
? replacer.filter(k => typeof k === 'string' || typeof k === 'number').map(String)
: Object.keys(obj);
const pairs = [];
for (const k of keys) {
const val = stringify(obj[k], k, obj);
if (val !== undefined) {
pairs.push(`${escapeString(k)}:${val}`);
}
}
return `{${pairs.join(',')}}`;
}
function escapeString(str) {
return '"' + str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/[\b]/g, '\\b')
.replace(/\f/g, '\\f') + '"';
}
return stringify(value, '', { '': value });
}Building logging utilities that need to serialize objects with custom handling for sensitive data (redacting passwords), circular references (Node.js util.inspect), and non-JSON types (BigInt, Map, Set).
Understanding JSON.stringify limitations helps explain why structuredClone is preferred for deep cloning — JSON.stringify drops undefined, functions, Symbols, and fails on circular references.
Extend the implementation to support the space parameter for pretty-printing with proper indentation at each nesting level.