All questions

JSON.stringify II

Premium

JSON.stringify II

The medium myJsonStringify handles the type-dispatch core — primitives, arrays, objects, the undefined-triple rule. This version layers on the three spec features production code depends on: a replacer (filter or transform), an indent (pretty-printing), and circular-reference detection that throws like native JSON.stringify. Defer to the medium version for the basics; this question is about layering the new features cleanly on top.

Signature

function myJsonStringifyII(
  value: unknown,
  replacer?: ((key: string, value: unknown) => unknown) | string[] | null,
  indent?: number | string | null,
): string | undefined;

Examples

// Compact mode (same behaviour as the medium version).
myJsonStringifyII({ a: 1, b: 2 });
// '{"a":1,"b":2}'

// Replacer as an allow-list array — only listed keys survive.
myJsonStringifyII({ a: 1, b: 2, c: 3 }, ['a', 'c']);
// '{"a":1,"c":3}'

// Replacer as a function — runs for every key/value pair, return value
// is what gets serialized. Return undefined to drop a key.
myJsonStringifyII({ a: 1, b: 2 }, (key, value) =>
  typeof value === 'number' ? value * 10 : value,
);
// '{"a":10,"b":20}'

// Numeric indent — newlines between entries, `indent` spaces per level.
myJsonStringifyII({ a: 1, b: [2, 3] }, null, 2);
// '{\n  "a": 1,\n  "b": [\n    2,\n    3\n  ]\n}'

// String indent — used verbatim (clipped to 10 chars).
myJsonStringifyII({ a: 1 }, null, '\t');
// '{\n\t"a": 1\n}'

// Circular reference — throws, just like native JSON.stringify.
const a = {};
a.self = a;
myJsonStringifyII(a);
// TypeError: Converting circular structure to JSON

Notes

  • Replacer dispatch — if replacer is an array, treat it as a string allow-list applied at every object (not just the root). If it's a function, call it with (key, value) for every key/value pair, including the root keyed as the empty string "". Anything else (including null/undefined) means no filtering.
  • Replacer return values — whatever the function returns is what gets serialized in place of value. Returning undefined drops the key from an object (or becomes "null" inside an array, mirroring the medium version's array-item rule).
  • Indent normalization — a number n in [1, 10] becomes n spaces; numbers outside that range are clipped (above 10) or ignored (zero, negative, NaN). A string is used verbatim but clipped to its first 10 characters. Anything else means compact mode.
  • Indent semantics — each nesting level multiplies the indent unit by depth. Empty {} and [] stay compact (no inner newline) even with indent set.
  • Cycle detection is PATH-based — track the set of ancestor objects on the current descent. Add on entry, remove on ascent. A DAG (same object reachable via two non-overlapping branches) must NOT throw — only a true cycle (an ancestor) does.
  • toJSON hook — if a value's own .toJSON is a function, call it with the key and serialize its return value instead. This runs BEFORE the replacer (matches spec ordering).
  • Out of scopeBigInt (would normally throw), wrapped primitive objects (new Number(1)), and the spec's "well-known symbol" branches.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium