@ferrow/stable-stringify
v1.0.0
Published
Deterministic JSON.stringify with recursively sorted keys, cycle detection, key filtering/replacer, stable number formatting, a streaming generator API, and a hashObject(obj) -> sha256 helper for cache keys and dedup.
Maintainers
Readme
stable-stringify
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).
Why
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.
Install
npm install stable-stringifyQuickstart
import { 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 permutationAPI
stableStringify(value: unknown, options?: StringifyOptions): string | undefined
Deterministic stringify. Returns undefined for the same top-level cases
JSON.stringify does (undefined, a function, a symbol).
stableStringifyChunks(value: unknown, options?: StringifyOptions): Generator<string>
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.
hashObject(value: unknown, options?: StringifyOptions): string
SHA-256 hex digest of stableStringify(value, options) (via Node's
crypto module).
formatNumber(n: number): string
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]"
}Limits
- 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
