Data integrity guide
Stable, safe, and canonical JSON are different tools
“Deterministic JSON” can mean a repeatable cache key, a log line that never crashes, or bytes suitable for a signature. Those contracts overlap, but they are not interchangeable.
Short answer: use ordinary JSON.stringify for normal JSON output, stable sorting for semantically equivalent cache keys, safe mode for diagnostics, and RFC 8785 canonicalization only when systems must agree on exact bytes.
1. Pick the contract, not the package name
| Need | Mode | Expected behavior |
|---|---|---|
| API request body | JSON.stringify | Standard JavaScript JSON semantics; key insertion order is preserved. |
| Cache or memoization key | Stable stringify | Object keys sort recursively; equivalent plain data produces the same string. |
| Error logging | Safe stringify | Cycles, BigInt, getters, and size limits become controlled output. |
| Cross-system signature | RFC 8785 canonicalization | Strict input validation and interoperable canonical UTF-8 bytes. |
Do not use a cycle marker in a signature. Do not let an unbounded compatible serializer process hostile diagnostics. Do not pay canonicalization constraints for a normal response body that does not need them.
2. Stable JSON makes key order independent
Modern JavaScript specifies property enumeration order, so JSON.stringify is repeatable for the same object. Two objects with the same key-value pairs can still serialize differently when their properties were inserted in a different order.
const first = {};
first.region = 'ca';
first.page = 2;
const second = {};
second.page = 2;
second.region = 'ca';
JSON.stringify(first) === JSON.stringify(second); // false
A stable serializer sorts object keys at every level while preserving array order:
import stringify from '@stackline/stable-stringify';
const firstKey = stringify(first);
const secondKey = stringify(second);
firstKey === secondKey; // true
// {"page":2,"region":"ca"}
This works well for local caches, memoization, snapshot identity, deduplication, and non-cryptographic content comparisons. The default export follows the established fast-json-stable-stringify calling shape.
Stable property order does not make arbitrary JavaScript objects equivalent. Dates, custom toJSON, getters, sparse arrays, numbers, and class instances still have serialization semantics that the application must define.
3. Safe stringify is deliberately diagnostic
Logs and error handlers often run precisely when application state is unusual. A serializer that throws on BigInt, cycles, a hostile getter, or oversized data can hide the original failure. Safe mode favors bounded, valid diagnostic output over lossless round trips.
import { safeStringify } from '@stackline/stable-stringify';
const event = { requestId: 42n, user: { id: 'u-7' } };
event.context = event;
const line = safeStringify(event);
// {"context":"[Circular]","requestId":"42","user":{"id":"u-7"}}
The default budgets cap depth, visited entries, and output length. Tighter limits are appropriate before writing user-controlled state to centralized logging:
const line = safeStringify(value, null, 0, {
depthLimit: 20,
edgesLimit: 5_000,
maxLength: 250_000
});
Because cycle markers and truncation are representations, safe output should not be parsed later as an authoritative copy of the original value.
4. Canonical JSON produces shared bytes
Digital signatures and content-addressed storage need more than sorted keys. Independent implementations must agree on accepted data, string escaping, key ordering, and number serialization. RFC 8785, also called JSON Canonicalization Scheme (JCS), defines that contract for I-JSON data.
import { createHash } from 'node:crypto';
import { canonicalizeBytes } from '@stackline/stable-stringify';
const document = {
currency: 'CAD',
amount: 4.50,
active: true
};
const digest = createHash('sha256')
.update(canonicalizeBytes(document))
.digest('hex');
Canonical mode rejects values that cannot satisfy the standard, including non-finite numbers, BigInt, undefined values, cycles, sparse arrays, lone UTF-16 surrogates, accessors, symbols, and class instances. It does not invoke getters or toJSON.
Validate the business document before canonicalizing it. Canonicalization makes bytes reproducible; it does not prove that fields are authorized, complete, fresh, or meaningful.
5. Put resource limits at exposed boundaries
Compatibility-oriented stable mode is unlimited by default. That matches legacy expectations but is not a good policy for arbitrary request data. Set limits or use safe mode when depth, entry count, accessors, or output size is not already controlled.
import { configure } from '@stackline/stable-stringify';
const cacheKey = configure({
maxDepth: 50,
maxEntries: 10_000,
maxLength: 500_000,
accessors: 'throw'
});
cacheKey({ route: '/users', query: { page: 2 } });
Limits should fail predictably and expose a stable error code. Avoid timeout-only tests: deterministic depth, entry, and output limits are more reliable under a loaded CI runner.
6. Preserve an existing import during migration
npm install @stackline/stable-stringifyView on npmDirect imports expose stable, safe, and canonical modes:
import stringify, {
safeStringify,
canonicalize,
canonicalizeBytes
} from '@stackline/stable-stringify';
An npm alias lets an existing codebase test the compatible default API without changing import statements:
npm uninstall fast-json-stable-stringify
npm install fast-json-stable-stringify@npm:@stackline/stable-stringify
Run differential tests over representative values, comparators, replacers, toJSON hooks, unsupported values, indentation, and cycles before adopting any drop-in replacement.