Stackline Open Source

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.

Updated August 21, 202610 minute readJavaScript and TypeScript

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

NeedModeExpected behavior
API request bodyJSON.stringifyStandard JavaScript JSON semantics; key insertion order is preserved.
Cache or memoization keyStable stringifyObject keys sort recursively; equivalent plain data produces the same string.
Error loggingSafe stringifyCycles, BigInt, getters, and size limits become controlled output.
Cross-system signatureRFC 8785 canonicalizationStrict 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.

Semantic boundary

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.

Cryptographic rule

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 npm

Direct 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.