npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@opentf/serialize

v0.1.0

Published

Type-preserving JavaScript serialization: Date, Map, Set, typed arrays, BigInt, Temporal, cycles and more, over plain JSON.

Readme

Serialize

Type-preserving JSON for JavaScript: readable on the wire, safe on untrusted input

npm | JSR | Wire format

An Open Tech Foundation project

JSON.stringify turns a Date into a string, a Map into {} and a BigInt into an exception. @opentf/serialize keeps them — plus typed arrays, Temporal, undefined, NaN/-0 and circular references — while plain data stays plain JSON.

import { parse, stringify } from "@opentf/serialize";

const text = stringify({ at: new Date(0), tags: new Set(["a"]), big: 10n });
// {"v":{"at":"@1","tags":"@2","big":"@3"},"$refs":{"1":{"_t":"Date","_v":"1970-01-01T00:00:00.000Z"},…}}

const value = parse(text, { maxLength: 1_000_000, maxDepth: 64 });
value.at.getFullYear(); // a live Date again

[!NOTE] Pre-1.0. The API can change between minor versions. The wire format is the one obj-diff 0.18 writes, byte for byte.

Why Serialize

| Need | How Serialize meets it | | --- | --- | | 👀 Readable on the wire | Plain data stays as it is; only special values become "@n" tokens, so a network panel still shows your object. | | 🔒 Safe on hostile input | __proto__ keys become own properties, never prototypes; every $refs entry is shape-checked; malformed input is an error, never undefined. | | 🚧 Bounded work | Opt-in maxLength and maxDepth stop oversized or deeply nested bodies before they cost a stack overflow. | | 🏷️ Errors you can branch on | One SerializeError class with a code — map INPUT_TOO_LARGE to 413 and the rest to 400. | | 📦 No dependencies | One small ESM + CJS package with its own .d.ts; runs anywhere JSON does. |

Compared with other libraries

| | @opentf/serialize | superjson | devalue | seroval (JSON mode) | @ungap/structured-clone | | --- | --- | --- | --- | --- | --- | | Wire shape | Your JSON + $refs table | Your JSON + meta paths | Flattened slot array | Node tree | Flattened records | | Plain data stays readable | Yes | Yes | No | No | No | | Own __proto__ key | Kept as own key | Rejected (throws) | Rejected (throws) | Kept as own key | Kept as own key | | Wrong-shape input | Throws | Returns undefined | Throws | Throws | Throws | | Depth limit | maxDepth (opt-in) | None | None | depthLimit (1000) | None | | Input size limit | maxLength (opt-in) | None | None | None | None | | Cycles through Map/Set | Yes | Yes | Yes | Yes | Yes | | Custom classes | No | registerClass | reducers / revivers | Plugins | No | | Runtime dependencies | 0 | 1 | 0 | 0 | 0 |

Serialize trades a little speed and size for a format you can read and a parser you can point at a request body. When you need custom classes, use seroval or devalue.

Measured

Measured 2026-11-04 on ES-Runtime/0.46.0 (Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz, 12 threads) against superjson 2.2.6, devalue 6.0.3, seroval 1.7.7, @ungap/structured-clone 1.4.0.

Round-trip fidelity

Each value goes through stringify then parse and must come back with the same type, value, keys and identity. ✅ exact · ❌ different value or type · 💥 throws.

| Value | @opentf/serialize | superjson | devalue | seroval | @ungap/structured-clone | | --- | --- | --- | --- | --- | --- | | Plain nested object | ✅ | ✅ | ✅ | ✅ | ✅ | | Date | ✅ | ✅ | ✅ | ✅ | ✅ | | Invalid Date | ✅ | ❌ | ✅ | ✅ | ✅ | | RegExp | ✅ | ✅ | ✅ | ✅ | ✅ | | Map | ✅ | ✅ | ✅ | ✅ | ✅ | | Set | ✅ | ✅ | ✅ | ✅ | ✅ | | BigInt | ✅ | ✅ | ✅ | ✅ | ✅ | | Int8Array | ✅ | ✅ | ✅ | ✅ | ✅ | | Float64Array [NaN, -0, ∞] | ✅ | ❌ | ✅ | ✅ | ❌ | | BigInt64Array | ✅ | 💥 | ✅ | ✅ | 💥 | | ArrayBuffer | ✅ | ❌ | ✅ | ✅ | ✅ | | DataView | ✅ | ❌ | ✅ | ✅ | ✅ | | Error + custom prop | ✅ | ❌ | 💥 | ✅ | ❌ | | Boxed Number | ✅ | ❌ | ✅ | ✅ | ✅ | | Boxed String | ✅ | ❌ | ✅ | ✅ | ✅ | | undefined | ✅ | ✅ | ✅ | ✅ | ✅ | | NaN | ✅ | ✅ | ✅ | ✅ | ❌ | | -0 | ✅ | ✅ | ✅ | ✅ | ✅ | | Infinity | ✅ | ✅ | ✅ | ✅ | ❌ | | URL | ✅ | ✅ | ✅ | 💥 | ❌ | | Temporal.PlainDate | ✅ | ❌ | ✅ | ✅ | ❌ | | Temporal.ZonedDateTime | ✅ | ❌ | ✅ | ✅ | ❌ | | Shared reference | ✅ | ✅ | ✅ | ✅ | ✅ | | Circular object | ✅ | ✅ | ✅ | ✅ | ✅ | | Circular array | ✅ | ✅ | ✅ | ✅ | ✅ | | Cycle through a Map | ✅ | ✅ | ✅ | ✅ | ✅ | | Sparse array | ✅ | ❌ | ✅ | ✅ | ❌ | | Own proto key | ✅ | 💥 | 💥 | ✅ | ✅ | | Score | 28/28 | 16/28 | 26/28 | 27/28 | 19/28 |

Malformed input

What parse does with valid JSON that is not its own format. Throwing is the safe answer; a quiet undefined reaches application code as if it were data.

| parse(...) input | @opentf/serialize | superjson | devalue | seroval | @ungap/structured-clone | | --- | --- | --- | --- | --- | --- | | Empty object {} | throws | ⚠️ returns undefined | throws | throws | throws | | Foreign object {"x":1} | throws | ⚠️ returns undefined | throws | throws | throws | | Bare number 42 | throws | ⚠️ returns undefined | throws | throws | throws |

Wire size

UTF-8 bytes of the output; smallest in bold.

| Value | @opentf/serialize | superjson | devalue | seroval | @ungap/structured-clone | | --- | --- | --- | --- | --- | --- | | Flat object | 41 | 44 | 43 | 126 | 83 | | Mixed native types | 171 | 153 | 108 | 268 | 146 | | Typed array (8) | 71 | 96 | 58 | 171 | 55 | | Nested record | 192 | 192 | 133 | 270 | 182 | | Tricky numbers | 132 | 115 | 24 | 113 | 74 | | Circular graph | 67 | 83 | 25 | 104 | 49 |

Speed

Typed record (ops/sec, higher is better)

| Library | stringify | parse | | --- | --- | --- | | @opentf/serialize | 135,894 | 159,406 | | superjson | 53,343 | 165,938 | | devalue | 185,374 | 241,824 | | seroval | 199,268 | 111,388 | | @ungap/structured-clone | 163,472 | 156,489 |

1,000 plain rows (ops/sec, higher is better)

| Library | stringify | parse | | --- | --- | --- | | @opentf/serialize | 1,443 | 1,585 | | superjson | 212 | 3,145 | | devalue | 577 | 1,085 | | seroval | 744 | 472 | | @ungap/structured-clone | 621 | 545 | | JSON | 6,078 | 3,122 |

1,000 typed rows (ops/sec, higher is better)

| Library | stringify | parse | | --- | --- | --- | | @opentf/serialize | 397 | 387 | | superjson | 108 | 317 | | devalue | 409 | 692 | | seroval | 503 | 419 | | @ungap/structured-clone | 370 | 474 |

Built for

| Project | Use | | --- | --- | | obj-diff | Its diff serialize / deserialize and value stringify / parse, extracted here unchanged in format. obj-diff's own test suite passes against a thin wrapper over encode / decode. | | AFW server functions | The planned body and result encoding (docs/SERVER_FUNCTIONS.md §3.2), which needs a decoder that is safe for untrusted bodies. |

Install

pnpm add @opentf/serialize
npm install @opentf/serialize   # or: bun add, deno add jsr:@opentf/serialize

Supported values

| Kind | Values | | --- | --- | | Primitives | string, number, boolean, null, undefined, bigint, NaN, ±Infinity, -0 | | Built-ins | Date (incl. invalid), RegExp, URL, Map, Set, boxed Number / String / Boolean | | Errors | Error and its standard subclasses, with name, message and own properties | | Binary | Every typed array, ArrayBuffer, DataView — byte-exact, as base64 | | Temporal | Any Temporal.* value; decoding needs globalThis.Temporal | | Graphs | Cycles and shared identity through objects, arrays, Map, Set and Error; any value used twice decodes to one object | | Sparse arrays | Holes stay holes, distinct from undefined and null |

Symbols, functions and class instances throw.

Wire format

stringify writes an envelope {"v": <value>, "$refs"?: {...}}:

| Piece | Rule | | --- | --- | | Plain data | Written as ordinary JSON. User keys named _t / _v are just data. | | Special values | Replaced by a token "@n"; $refs["n"] holds { "_t": <type>, "_v": <payload> }. | | Strings starting with @ | Escaped with one more @, so they never read as tokens. | | Shared or cyclic values | Written once in $refs (plain containers as an obj / arr entry) and referenced by token everywhere; single-use plain containers stay inline. | | Array holes | Every hole is a token for one shared {"_t":"hole"} entry. | | No special values | No $refs key at all: {"v":{"a":1}}. |

Untrusted input

import { parse, SerializeError } from "@opentf/serialize";

try {
  return parse(body, { maxLength: 1_000_000, maxDepth: 64 });
} catch (err) {
  if (err instanceof SerializeError) return new Response(null, { status: err.code === "INPUT_TOO_LARGE" ? 413 : 400 });
  throw err;
}

| Option | Effect | Default | | --- | --- | --- | | maxLength | Longest input, in UTF-16 code units (text.length), checked before JSON.parse | Unlimited | | maxDepth | Deepest container nesting — arrays, objects, Map, Set, Error props, hoisted ones included. [[1]] is 2 | Unlimited |

stringify(value, { maxDepth }) takes the same depth limit.

Errors

Every failure is a SerializeError (a subclass of Error) whose code is one of:

| code | Meaning | | --- | --- | | INVALID_INPUT | parse was given something other than a string | | INVALID_JSON | The text is not JSON (cause holds the SyntaxError) | | INPUT_TOO_LARGE | Longer than maxLength | | INVALID_ENVELOPE | Not {"v": ...}, or $refs is not an object | | MAX_DEPTH | Nests deeper than maxDepth | | MISSING_REF | A "@n" token with no $refs entry | | UNKNOWN_TYPE | An unrecognised _t tag | | INVALID_PAYLOAD | An entry's _v is the wrong shape for its type | | CIRCULAR_REF | A forged entry that refers to itself where no cycle is possible (e.g. a boxed Number) | | UNSUPPORTED_VALUE | A symbol, function or class instance given to stringify | | TEMPORAL_UNAVAILABLE | Temporal data, but no globalThis.Temporal | | INVALID_OPTION | A limit that is not a non-negative integer or Infinity |

API

| Export | Purpose | | --- | --- | | stringify(value, options?) | Any value → JSON text | | parse(text, options?) | JSON text → value; validates everything it reads | | encode(roots, options?) | Low level: encode several values that share one $refs table, to embed in your own JSON | | decode(values, refs, options?) | Low level: the inverse of encode | | SerializeError | The error class; see Errors |

encode / decode are how obj-diff writes each diff op as {type, path, value, $refs}:

const { values: [path, value], refs } = encode([op.path, op.value]);
const wire = JSON.stringify({ type: op.type, path, value, $refs: refs });

const raw = JSON.parse(wire);
const [p, v] = decode([raw.path, raw.value], raw.$refs, { maxDepth: 64 });

Develop

pnpm install
tsr ci            # build, tests on src/ and dist/, typecheck, lint, format check
tsr test          # tests against src/
tsr test:dist     # build, then the tests against dist/
tsr bench:readme  # run the benchmarks and regenerate the Measured section above

Tests, builds and benchmarks run on esdev; tasks are defined in tasks.toml and run with tsr.

License

MIT — see LICENSE.