Skip to content

mirkokiefer/canonical-json

Repository files navigation

npm version tests

canonical-json - Deterministic JSON.stringify()

A drop-in replacement for JSON.stringify that produces deterministic, canonical JSON output compliant with RFC 8785 (JSON Canonicalization Scheme).

Ideal for content-addressable hashing, digital signatures, and distributed systems where identical objects must produce identical bytes.

Install

npm install canonical-json

Usage

import stringify from 'canonical-json'

stringify({ b: 2, a: 1, c: { y: 0, x: 9 } })
// '{"a":1,"b":2,"c":{"x":9,"y":0}}'

Replacer

Supports both function and array replacers, just like JSON.stringify:

stringify({ a: 1, b: 2, c: 3 }, ['a', 'c'])
// '{"a":1,"c":3}'

stringify({ a: 1, b: 2 }, (key, value) => key === 'b' ? undefined : value)
// '{"a":1}'

Indentation

stringify({ a: 1 }, null, 2)
// '{\n  "a": 1\n}'

Custom Key Order

Pass a comparator function as the fourth argument:

const order = { first: 1, second: 2, third: 3 }
const cmp = (a, b) => (order[a] || 9999) - (order[b] || 9999)

stringify({ third: 'c', first: 'a', second: 'b' }, null, null, cmp)
// '{"first":"a","second":"b","third":"c"}'

Streaming with walk

walk traverses the value and streams canonical JSON chunks to a callback — without building the full string:

import { createHash } from 'node:crypto'
import { walk } from 'canonical-json'

const hasher = createHash('sha256')
walk(obj, chunk => hasher.update(chunk))
const digest = hasher.digest('hex')

Convenience hashing

For Node.js/Bun/Deno, a separate entry point provides one-liner hashing without pulling node:crypto into browser builds:

import { hash } from 'canonical-json/hash'

hash(obj)              // sha256 hex digest
hash(obj, 'md5')       // md5 hex digest
hash(obj, 'sha512')    // sha512 hex digest

API

// canonical-json
export default function stringify(
  value: any,
  replacer?: ((key: string, value: any) => any) | string[],
  space?: string | number,
  keyCompare?: (a: string, b: string) => number
): string | undefined

export function walk(
  value: any,
  write: (chunk: string) => void,
  keyCompare?: (a: string, b: string) => number
): void

// canonical-json/hash (Node.js/Bun/Deno only)
export function hash(
  value: any,
  algorithm?: string,    // default: 'sha256'
  keyCompare?: (a: string, b: string) => number
): string               // hex digest

RFC 8785 Compliance

When called without replacer, space, or keyCompare, the output conforms to RFC 8785 (JCS):

  • Object keys sorted by UTF-16 code unit order
  • Numbers serialized per ES6 Number.toString() rules (-0 becomes 0)
  • Only mandatory characters escaped (U+0000-U+001F, ", \)
  • No whitespace

CLI

# Canonicalize
echo '{"b":2,"a":1}' | canonical-json
# {"a":1,"b":2}

# Content hash (default sha256)
echo '{"b":2,"a":1}' | canonical-json hash
# 43258cff783fe7036d8a43033f830adfc60ec037...

# Hash with specific algorithm
echo '{"b":2,"a":1}' | canonical-json hash --algo md5

# Hash multiple files
canonical-json hash *.json

# Verify files are canonical (exit 1 if not)
canonical-json verify data.json
canonical-json verify *.json

Test

npm test

Zero dependencies. Tests use Node's built-in test runner.

License

MIT

About

Deterministic JSON.stringify() with streaming — RFC 8785 compliant, zero dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors