Deterministic JSON.stringify for TypeScript/JavaScript: recursively sorted
object keys, cycle detection, key filtering/replacer, stable number
formatting, a streaming generator API for huge objects, and a
hashObject(obj) helper. Zero runtime dependencies (uses Node's built-in
crypto for hashing only).
Regular JSON.stringify preserves insertion order, so two objects with the
same data but different key order produce different strings — useless as a
cache key, signature, or dedup fingerprint. stable-stringify sorts keys at
every level so equivalent objects always serialize identically.
npm install stable-stringifyimport { stableStringify, hashObject } from "stable-stringify";
stableStringify({ b: 2, a: 1 }); // '{"a":1,"b":2}'
stableStringify({ a: 1, b: 2 }); // '{"a":1,"b":2}' — identical regardless of input order
hashObject({ b: 2, a: 1 }); // sha256 hex digest, same for any key-order permutationDeterministic stringify. Returns undefined for the same top-level cases
JSON.stringify does (undefined, a function, a symbol).
Same serialization, yielded incrementally instead of built into one string — for piping/consuming a huge object's output without holding it all in memory at once.
SHA-256 hex digest of stableStringify(value, options) (via Node's
crypto module).
The number formatter used internally: NaN/Infinity/-Infinity → "null"
(matches JSON.stringify), -0 → "0", otherwise String(n).
interface StringifyOptions {
replacer?: (key: string, value: unknown) => unknown; // called after toJSON(), like JSON.stringify's function replacer
keys?: readonly string[]; // allow-list of keys to include, at every level
cycles?: "error" | "placeholder"; // default "error"
cyclePlaceholder?: string; // default "[Circular]"
}- No
space/pretty-print option — output is always compact, since the point is a stable machine-comparable string, not human formatting. keysfilters every object level identically (same semantics asJSON.stringify's array-form replacer) — there's no per-path filtering.- Cycle detection tracks the current recursion path (ancestors), not the whole object graph — a value referenced twice from siblings (not an ancestor) is serialized twice, which is correct JSON semantics, not a cycle.
hashObjecthashes the stringified form; two semantically-equal values that stringify differently (e.g. aMapvs a plain object) will hash differently.
Part of the ferrow-toolkit collection · Sponsored by Ferrow