All questions

Superjson

Premium

Superjson

JSON.stringify and JSON.parse are the default way to send JavaScript values over the wire or stash them in localStorage. But JSON only knows about strings, numbers, booleans, null, plain objects, and arrays. Hand it a Date, a Map, a Set, or a BigInt and it quietly mangles them — a Date comes back as a string, a Map comes back as {}, and a BigInt makes JSON.stringify throw outright.

Implement serialize and deserialize so the round-trip preserves both structure and type. After deserialize(serialize(value)), a Date is still a Date (same instant), a Map is still a Map (same entries, same insertion order), a BigInt is still a bigint, and undefined / NaN / Infinity survive instead of being dropped or flattened to null.

Signature

// serialize: turn any supported value into a single JSON string.
function serialize(value: unknown): string;

// deserialize: turn that string back into the original value, types intact.
function deserialize(str: string): unknown;

// The contract that must hold for every supported value:
//   deserialize(serialize(value))  deep-equals  value
//   ...and revived special values keep their type (instanceof Date, typeof bigint, ...).

The returned string must be valid JSON — JSON.parse(serialize(value)) must not throw. How you pack the type information into that string is up to you.

Examples

// A Date keeps its type and its exact instant.
const str = serialize(new Date('2026-06-04T12:30:00.000Z'));
const back = deserialize(str);
back instanceof Date;     // true
back.getTime();           // 1780576200000  (same instant)

// Plain JSON would have lost this:
JSON.parse(JSON.stringify(new Date())) instanceof Date; // false — it's a string
// A nested mixture: a Map (with an object value) inside an object inside an array.
const value = [
  { label: 'config', data: new Map([['retries', 3]]), when: new Date(0) },
];
const back = deserialize(serialize(value));
back[0].data instanceof Map;        // true
back[0].data.get('retries');        // 3
back[0].when instanceof Date;       // true

// And the values JSON silently eats:
deserialize(serialize({ a: undefined, b: NaN, c: Infinity, d: 9n }));
// → { a: undefined, b: NaN, c: Infinity, d: 9n }   (all four preserved)

Notes

  • In scope: Date, Map, Set, BigInt, undefined, NaN, Infinity, -Infinity, and any nesting of these inside plain objects and arrays. Primitives (string, finite number, boolean, null) pass through unchanged.
  • Insertion order matters. A revived Map or Set must iterate its entries in the same order they were inserted.
  • undefined must survive everywhere — as the whole value, as an object property (the key stays present), and as an array element (the slot stays at its index).
  • A string is just a string. If the input happens to contain a string that looks like one of your type tags, deserializing must not mistake it for a special value. Keep the type metadata out of the data itself.
  • Don't worry about functions, symbols, class instances beyond the built-ins above, circular references, or referential identity (two slots pointing at the same Map) — those are out of scope here, though a few appear under Going further in the solution.

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