@cqrs-ddd/safe-stringify
Two JSON serializers with opposite goals, and the key-segment helpers that build identity keys. No runtime dependencies and no framework.
Strict: stableStringify, toStrictJsonValue |
Safe: safeStringify, safeSanitize, redactValue |
|
|---|---|---|
| Use for | identity: cache keys, idempotency fingerprints, stored snapshots | display: logs, audit and dead-letter payloads |
| Output | deterministic JSON, object keys sorted at every level, stable across releases | readable JSON in insertion order; the format may evolve |
| Unsupported input | throws a TypeError |
never throws |
| Secrets | never redacts, because a key must not change | excludes and redacts on request |
Use the strict serializer for anything that identifies data. Never use safeStringify
for a key or a fingerprint: its output is not sorted, it replaces values it cannot
represent, and its format may change.
Installation
Section titled “Installation”npm install @cqrs-ddd/safe-stringify# orpnpm add @cqrs-ddd/safe-stringifyRequires Node.js 22.12 or later.
Published as an ES module; a CommonJS application loads it with require(). Coming from
0.3.x, see Upgrading from 0.3.x.
Strict serializer
Section titled “Strict serializer”import { stableStringify, toStrictJsonValue } from '@cqrs-ddd/safe-stringify';
stableStringify({ z: 1, a: { d: 2, c: new Date(0) } });// '{"a":{"c":"1970-01-01T00:00:00.000Z","d":2},"z":1}'
toStrictJsonValue({ b: 2, a: 1 }, true); // { a: 1, b: 2 }, a null-prototype object- Strings, booleans,
nulland finite numbers are kept. Dates become ISO-8601 strings. An object withtoJSON()is replaced by what that method returns. - Everything else throws a
TypeError:undefined, functions,bigint, symbols and symbol-keyed properties, non-finite numbers, invalid dates, cycles, sparse arrays,Map,Set,WeakMap,WeakSet,Error,RegExp,Promise,ArrayBufferand typed arrays. stableStringifywraps that failure in aTypeErrorwhosecauseis the original.- Structurally equal values produce byte-identical strings, whatever their property
insertion order. Keys sort by UTF-16 code unit, as
Array.prototype.sortdoes.
Safe serializer
Section titled “Safe serializer”import { DEFAULT_REDACT_KEYS, redactValue, safeStringify,} from '@cqrs-ddd/safe-stringify';
const payload: Record<string, unknown> = { user: 'jane', password: 'secret', amount: 10n };payload.self = payload;
safeStringify(payload);// '{"user":"jane","password":"secret","amount":"[bigint]","self":"[Circular]"}'
safeStringify(payload, { redactKeys: DEFAULT_REDACT_KEYS });// '{"user":"jane","password":"[REDACTED]","amount":"[bigint]","self":"[Circular]"}'
redactValue(payload); // a deep clone with DEFAULT_REDACT_KEYS masked-
safeStringify(value, options?, indent?)andsafeSanitize(value, options?)never throw. Cycles become"[Circular]", errors are expanded toname,message,stackand their own enumerable properties, and values JSON cannot hold are replaced by a readable marker ("[bigint]","[Function]","[Invalid Date]","[Binary Data]","[Stream]"). ASet<string>in place ofoptionsis read asexcludeKeys.safeStringify(undefined)returns the string'undefined'. -
They redact nothing unless you pass
redactKeys.redactValue(value, keys?)appliesDEFAULT_REDACT_KEYSby default and keeps rich types (mode: 'clone'). -
SanitizeOptions:Option Effect excludeKeyskeys or dot-paths removed from the output; exact, case-sensitive match redactKeyskeys or dot-paths masked; matched ignoring case, _and-, sorefreshTokenalso masksrefresh_tokenredactReplacementthe mask; default REDACTED("[REDACTED]")mode'json'(default) turns rich types into JSON-friendly values;'clone'deep-clones them keeping their classes -
Exclusion and redaction, with a key matched at any depth or a dot-path matched at one place only:
safeStringify({ user: { profile: { email: 'x' }, token: 't' } },{ excludeKeys: ['user.profile'], redactKeys: ['token'], redactReplacement: '***' },);// '{"user":{"token":"***"}}'safeStringify({ when: new Date(0), tags: new Set(['a']), limits: new Map([['k', 1]]) });// '{"when":"1970-01-01T00:00:00.000Z","tags":["a"],"limits":{"k":1}}' -
DEFAULT_REDACT_KEYSlists common secret names (passwords, tokens, API keys, authorization headers, cookies, card data). No list recognizes every secret: add your application’s own names.
Key segments
Section titled “Key segments”import { ABSENT_SEGMENT, escapeKeySegment, joinKeySegments,} from '@cqrs-ddd/safe-stringify';
joinKeySegments(['cache', 'tenant:a', undefined, 'user']);// 'cache:tenant\\:a:\\-:user'joinKeySegments(segments)escapes each segment and joins them with:.undefinedandnullbecomeABSENT_SEGMENT(\-), so['a', undefined]and['a', '']produce different keys.escapeKeySegment(value)escapes\first, then:. A segment that is literally\-is written\\-, so it never collides with an absent segment.
Build a cache key from a tenant, a request name and a strict fingerprint of the payload:
import { joinKeySegments, stableStringify } from '@cqrs-ddd/safe-stringify';import { createHash } from 'node:crypto';
function cacheKey(tenantId: string | undefined, name: string, payload: unknown): string { const fingerprint = createHash('sha256').update(stableStringify(payload)).digest('hex'); return joinKeySegments(['cache', tenantId, name, fingerprint]);}The strict serializer’s output and these key formats are part of the package contract: changing them would orphan every stored cache entry, rate-limit bucket and idempotency fingerprint. The package’s golden-output specs pin them.
Migrating from @nestjs-pipeline/core 0.1.x
Section titled “Migrating from @nestjs-pipeline/core 0.1.x”@nestjs-pipeline/core 0.1.x did not export any serializer or key-segment helper from its
entry point: safeStringify and safeSanitize lived in its internal
dist/helpers/safeStringify module, and stableStringify, toStrictJsonValue,
redactValue, DEFAULT_REDACT_KEYS, REDACTED and the key-segment helpers did not exist.
Code that deep-imported the internal module moves to this package:
// Before (0.1.x, an internal path)import { safeSanitize, safeStringify } from '@nestjs-pipeline/core/dist/helpers/safeStringify';
safeStringify(value, new Set(['token', 'ctx.sessionUser']), 2);
// After (0.2.0)import { safeSanitize, safeStringify } from '@cqrs-ddd/safe-stringify';
safeStringify(value, { excludeKeys: ['token', 'ctx.sessionUser'] }, 2);- The
Set<string>second argument is still accepted and meansexcludeKeys. safeStringify(undefined)now returns'undefined'instead ofundefined.- Use
stableStringify, notsafeStringify, for any key or fingerprint you build.
License
Section titled “License”Dual-licensed under AGPLv3 and a Commercial License. See the root
LICENSE and
COMMERCIAL_LICENSE.txt
for details.