JSON Canonicalization (RFC 8785)
JavaScript library comparison

A reproducible comparison of JavaScript/TypeScript libraries that implement — or claim to implement — the JSON Canonicalization Scheme (JCS), plus popular deterministic serializers as baselines. Prompted by erdtman/canonicalize#15. Every number on this page is generated by the scripts in this repository: run npm run all to reproduce.

Legend: RFC 8785 ✓ passes every vector and never returns invalid JSON · vectors ✓ · emits invalid JSON canonicalizes JSON correctly, but returns unparseable output for some JavaScript inputs · fails RFC 8785 vectors · baseline deterministic serializer, no RFC 8785 claim

Libraries

LibraryVersionDownloads/wkLicenseTSDependenciesPure JS/TSLast publishNotes
canonicalize RFC 8785 ✓5.0.02,390,684Apache-2.0none2026-09-08RFC 8785 reference JS implementation (cited in the RFC, Appendix G)
json-canonicalize fails RFC 8785 vectors2.0.0 (latest: 3.0.0)164,383MITnone2026-08-20TypeScript implementation, adds circular-reference support
canonical-json fails RFC 8785 vectors0.4.045,573MITnone2026-03-24Claims RFC 8785 compliance, adds streaming API
json-canon vectors ✓ · emits invalid JSON1.0.116,752Apache-2.0none2023-05-12JCS serializer
@truestamp/canonify fails RFC 8785 vectors2.1.08,747MIT Licensenone2023-01-19TypeScript JCS implementation
canonify fails RFC 8785 vectors2.1.1493MIT Licensenone2025-06-12Fork of @truestamp/canonify
json-freeze fails RFC 8785 vectors1.0.0286MITnone2026-04-21Claims RFC 8785 compliance
@hookwarden/canonical-json fails RFC 8785 vectors0.1.033Apache-2.0none2026-05-31Zero-dependency JCS encoder
@lit-protocol/flows-jcs fails RFC 8785 vectors0.1.07MITnone2026-06-29Deliberate JCS subset (rejects some inputs by design)
@substrate-system/json-canon vectors ✓ · emits invalid JSON0.1.47SEE LICENSE IN LICENSEnone2026-08-07Fork of json-canon
JSON.stringify baselineNative, non-deterministic key order — performance reference
@tufjs/canonical-json baseline2.0.05,205,846MITnone2023-08-17OLPC canonical JSON (not RFC 8785; no floats)
another-json baseline0.2.0787,187Apache-2.0none2015-07-30Matrix-style compact canonical JSON (not RFC 8785)
fast-json-stable-stringify baseline2.1.0137,533,391MITnone2019-12-14Deterministic key order, no RFC 8785 number/string rules
safe-stable-stringify baseline2.5.052,235,315MITnone2024-08-24Deterministic + safe (circular refs, BigInt), not RFC 8785
json-stable-stringify baseline1.3.09,543,030MIT5 direct / 20 transitive2025-04-22Deterministic key order (substack lineage), not RFC 8785

Compliance

Byte-for-byte comparison against three vector groups: the six official input/output pairs from cyberphone/json-canonicalization, 10,000 checksum-verified ES6 number-serialization vectors, and hand-written edge cases derived from the RFC text — including lone surrogates (which RFC 8785 §3.2.2.2 says MUST terminate a compliant implementation with an error) and non-finite numbers such as the literal {"v":1e400}, which parses to Infinity and must be rejected.

LibraryOfficial pairsES6 numbers (10k)Edge casesNotes
canonicalize RFC 8785 ✓6/610000/1000043/43
json-canonicalize fails RFC 8785 vectors6/610000/1000029/43
canonical-json fails RFC 8785 vectors4/610000/1000028/43
json-canon vectors ✓ · emits invalid JSON6/610000/1000043/43
@truestamp/canonify fails RFC 8785 vectors6/610000/1000032/43
canonify fails RFC 8785 vectors6/610000/1000032/43
json-freeze fails RFC 8785 vectors6/610000/1000038/43
@hookwarden/canonical-json fails RFC 8785 vectors6/610000/1000035/43
@lit-protocol/flows-jcs fails RFC 8785 vectors6/610000/1000035/43
@substrate-system/json-canon vectors ✓ · emits invalid JSON6/610000/1000043/43
JSON.stringify baseline3/610000/1000025/43
@tufjs/canonical-json baseline3/63730/1000028/43
another-json baseline5/60/1000016/43
fast-json-stable-stringify baseline6/610000/1000031/43
safe-stable-stringify baseline6/610000/1000031/43
json-stable-stringify baseline6/610000/1000031/43

Passing these vectors is necessary but not sufficient. They feed only JSON, so a library can score full marks here and still return unparseable output for ordinary JavaScript input — json-canon scores 43/43 yet turns {a: undefined, b: 1} into {,"b":1}. See Output validity; the badges above combine both results.

Deviation details

json-canonicalize — 10+ failing vectors
canonical-json — 12+ failing vectors
@truestamp/canonify — 10+ failing vectors
canonify — 10+ failing vectors
json-freeze — 5+ failing vectors
@hookwarden/canonical-json — 8+ failing vectors
@lit-protocol/flows-jcs — 8+ failing vectors
JSON.stringify — 13+ failing vectors
@tufjs/canonical-json — 23+ failing vectors
another-json — 21+ failing vectors
fast-json-stable-stringify — 10+ failing vectors
safe-stable-stringify — 10+ failing vectors
json-stable-stringify — 10+ failing vectors

Performance

Relative speed differs a lot depending on the shape of the JSON, so each payload shape is measured separately (with tinybench). Expand “Example of the JSON used in this test” under each chart to see what was serialized.

small-object

JWT-claims-like object, 10 keys, shallow — 0.3 kB serialized. Higher is better; native JSON.stringify (grey) is the non-canonical speed ceiling.

JSON.stringify baseline
5,565,200 ops/s
canonical-json fails RFC 8785 vectors
1,624,055 ops/s
safe-stable-stringify baseline
1,518,109 ops/s
json-canon vectors ✓ · emits invalid JSON
1,364,959 ops/s
@substrate-system/json-canon vectors ✓ · emits invalid JSON
1,338,349 ops/s
fast-json-stable-stringify baseline
1,290,562 ops/s
@truestamp/canonify fails RFC 8785 vectors
1,044,489 ops/s
canonify fails RFC 8785 vectors
1,031,035 ops/s
canonicalize RFC 8785 ✓
1,031,021 ops/s
@lit-protocol/flows-jcs fails RFC 8785 vectors
975,096 ops/s
json-canonicalize fails RFC 8785 vectors
966,842 ops/s
json-stable-stringify baseline
922,689 ops/s
another-json baseline
866,461 ops/s
@hookwarden/canonical-json fails RFC 8785 vectors
709,624 ops/s
json-freeze fails RFC 8785 vectors
613,540 ops/s
@tufjs/canonical-json baseline
519,311 ops/s
Example of the JSON used in this test
{
  "iss": "https://issuer.example.com",
  "sub": "user-1234567890",
  "aud": [
    "api://default",
    "api://admin"
  ],
  "exp": 1893456000,
  "nbf": 1755000000,
  "iat": 1755000000,
  "jti": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
  "name": "Jane Doe",
  "admin": false,
  "scopes": [
    "openid",
    "profile",
    "email"
  ]
}

large-array

1000-row array of flat records — 166.7 kB serialized. Higher is better; native JSON.stringify (grey) is the non-canonical speed ceiling.

JSON.stringify baseline
5,658 ops/s
canonical-json fails RFC 8785 vectors
2,033 ops/s
safe-stable-stringify baseline
2,026 ops/s
@substrate-system/json-canon vectors ✓ · emits invalid JSON
1,844 ops/s
json-canon vectors ✓ · emits invalid JSON
1,833 ops/s
fast-json-stable-stringify baseline
1,663 ops/s
@truestamp/canonify fails RFC 8785 vectors
1,490 ops/s
canonify fails RFC 8785 vectors
1,479 ops/s
canonicalize RFC 8785 ✓
1,394 ops/s
json-canonicalize fails RFC 8785 vectors
1,290 ops/s
@lit-protocol/flows-jcs fails RFC 8785 vectors
1,281 ops/s
json-stable-stringify baseline
1,255 ops/s
another-json baseline
1,148 ops/s
@hookwarden/canonical-json fails RFC 8785 vectors
963 ops/s
json-freeze fails RFC 8785 vectors
777 ops/s
@tufjs/canonical-json baseline
not supported: cannot encode 8388.32
Example of the JSON used in this test
[
  {
    "id": 0,
    "uuid": "e>;Ss[OAj*j3HoW-_B|#v%,2 yJ\\}6LA%Q)%",
    "name": "{l }&|R2`$iv",
    "email": "nJz&c}o\\Q#@example.com",
    "active": true,
    "score": 8388.32,
    "tags": [
      "T=uxJA",
      "!z=Ikr"
    ]
  },
  {
    "id": 1,
    "uuid": "wI3][cy3:@S\";n/iEGEpM~8(c!ej];?]*1e|",
    "name": "[*Vy\"VQdpHP&",
    "email": "qJ5WKVQ<4v@example.com",
    "active": true,
    "score": 5196.94,
    "tags": [
      "3.7n1|",
      "5m>=fv"
    ]
  },
  {
    "id": 2,
    "uuid": "CFmt@HuwSo-C=-=u8:s+3%``WjCq%`aq/T1E",
    "name": "CsWg!`(t!8)w",
    "email": ")]RO\"BPv-_@example.com",
    "active": true,
    "score": 1738.41,
    "tags": [
      "$NnR\\]",
      "Ka+K/_"
    ]
  },
  {
    "id": 3,
    "uuid": "\"bp5.BxCIP`H&V`@MYj%zs.BF!O|42\" TLOh",
    "name": "5Qki)Z]'M>x@",
    "email": ".ueGe8&fRl@example.com",
    "active": true,
    "score": 957.26,
    "tags": [
      "jgNC8T",
      "_=4*KA"
    ]
  },
  {
    "id": 4,
    "uuid": "_ (;ul|+&,o8>Z+UKODV>imJ')c=F5m&mEm=",
    "name": "o72M[ARnKV[.",
    "email": "x)Wv_EesE^@example.com",
    "active": false
… truncated (237,693 characters in total)

deep-nesting

100 levels of nested objects/arrays — 4.6 kB serialized. Higher is better; native JSON.stringify (grey) is the non-canonical speed ceiling.

JSON.stringify baseline
261,279 ops/s
canonical-json fails RFC 8785 vectors
41,858 ops/s
safe-stable-stringify baseline
39,534 ops/s
@substrate-system/json-canon vectors ✓ · emits invalid JSON
38,622 ops/s
json-canon vectors ✓ · emits invalid JSON
37,803 ops/s
fast-json-stable-stringify baseline
34,890 ops/s
canonify fails RFC 8785 vectors
29,712 ops/s
@truestamp/canonify fails RFC 8785 vectors
29,636 ops/s
another-json baseline
28,910 ops/s
canonicalize RFC 8785 ✓
28,365 ops/s
json-canonicalize fails RFC 8785 vectors
26,219 ops/s
@hookwarden/canonical-json fails RFC 8785 vectors
25,472 ops/s
@lit-protocol/flows-jcs fails RFC 8785 vectors
23,678 ops/s
json-stable-stringify baseline
20,034 ops/s
@tufjs/canonical-json baseline
14,510 ops/s
json-freeze fails RFC 8785 vectors
13,294 ops/s
Example of the JSON used in this test
{
  "level": 99,
  "z": "padding",
  "a": [
    99,
    100
  ],
  "child": {
    "level": 98,
    "z": "padding",
    "a": [
      98,
      99
    ],
    "child": {
      "level": 97,
      "z": "padding",
      "a": [
        97,
        98
      ],
      "child": {
        "level": 96,
        "z": "padding",
        "a": [
          96,
          97
        ],
        "child": {
          "level": 95,
          "z": "padding",
          "a": [
            95,
            96
          ],
          "child": {
            "level": 94,
            "z": "padding",
            "a": [
              94,
              95
            ],
            "child": {
              "level": 93,
              "z": "padding",
              "a": [
                93,
                94
              ],
              "child": {
                "level": 92,
                "z": "padding",
                "a": [
                  92,
                  93
                ],
                "child": {
                  "level": 91,
                  "z": "padding",
                  "a": [
               
… truncated (87,290 characters in total)

number-heavy

10,000 mixed-magnitude numbers — 130.8 kB serialized. Higher is better; native JSON.stringify (grey) is the non-canonical speed ceiling.

JSON.stringify baseline
7,762 ops/s
canonical-json fails RFC 8785 vectors
4,643 ops/s
another-json baseline
3,687 ops/s
fast-json-stable-stringify baseline
2,908 ops/s
@substrate-system/json-canon vectors ✓ · emits invalid JSON
2,647 ops/s
json-canon vectors ✓ · emits invalid JSON
2,644 ops/s
@hookwarden/canonical-json fails RFC 8785 vectors
2,616 ops/s
canonicalize RFC 8785 ✓
2,565 ops/s
canonify fails RFC 8785 vectors
2,524 ops/s
@truestamp/canonify fails RFC 8785 vectors
2,509 ops/s
safe-stable-stringify baseline
2,398 ops/s
@lit-protocol/flows-jcs fails RFC 8785 vectors
2,154 ops/s
json-stable-stringify baseline
1,858 ops/s
json-canonicalize fails RFC 8785 vectors
1,852 ops/s
json-freeze fails RFC 8785 vectors
1,730 ops/s
@tufjs/canonical-json baseline
not supported: cannot encode 0.33
Example of the JSON used in this test
[
  0.33,
  175752252,
  1091988679,
  2091352550,
  6908154499.251395,
  732494.9100147933,
  0.52,
  0.042174601648002866,
  9307232699356973000,
  347953.77077534795,
  0.26,
  0.52,
  455857.1302331984,
  1042499273,
  7.63817771570757e-18,
  0.4,
  547183.7874501944,
  1339503274,
  556824.6911279857,
  2047834226,
  0.92,
  186181.33966811,
  4.986616887617856e-19,
  0.95,
  1034353626,
  0.68,
  9.8292943299748e-10,
  0.36,
  701419.0000481904,
  514506296,
  78759436099790.03,
  988925263,
  9786.891078110784,
  708680.0376418978,
  0.3,
  0.77,
  2115131162,
  979310.2527037263,
  0.45,
  175008466,
  244671616,
  1170225474052131.2,
  0.68,
  382658.9207164943,
  9.100767713971436e-12,
  7299.932490568608,
  0.93,
  109510.81896200776,
  0.15,
  736871.9610385597,
  999460.5381507427,
  0.03,
  0.4,
  0.5,
  1482362742,
  1930612591,
  0.0005638101017102599,
  0.38,
  946102.3595649749,
  0.33,
  1623928620,
  1775626404,
  452931.9158755243,
  14131142105907.201,
  888780.0262309611,
  0.79,
  0.34,
  547691.5272884071,
  4.2823503050021827e-7,
  582315.8526327461,
  7.486
… truncated (163,946 characters in total)

string-heavy

500 keys, unicode + escape-heavy strings (64 chars each) — 68.5 kB serialized. Higher is better; native JSON.stringify (grey) is the non-canonical speed ceiling.

JSON.stringify baseline
22,418 ops/s
canonical-json fails RFC 8785 vectors
11,180 ops/s
fast-json-stable-stringify baseline
8,933 ops/s
canonify fails RFC 8785 vectors
8,812 ops/s
@truestamp/canonify fails RFC 8785 vectors
8,811 ops/s
json-canonicalize fails RFC 8785 vectors
8,620 ops/s
safe-stable-stringify baseline
8,308 ops/s
json-stable-stringify baseline
7,913 ops/s
canonicalize RFC 8785 ✓
7,848 ops/s
@tufjs/canonical-json baseline
7,803 ops/s
json-canon vectors ✓ · emits invalid JSON
7,211 ops/s
@substrate-system/json-canon vectors ✓ · emits invalid JSON
7,150 ops/s
@lit-protocol/flows-jcs fails RFC 8785 vectors
5,884 ops/s
another-json baseline
5,145 ops/s
json-freeze fails RFC 8785 vectors
1,279 ops/s
@hookwarden/canonical-json fails RFC 8785 vectors
1,224 ops/s
Example of the JSON used in this test
{
  "ƅqpĵ😋亽Ãh": "😍亹a丵dzc乑zkwƠuãœuÂ😐we乇\u0000trcņyt\tw五\u0016cng丣i交cysÚ仮Ükk😎不丙vpjāýzijÉŧdiƲw么e",
  "f😕uh😥\u0005uh": "丁ŇgƏpĭfjbĴjjlrg丑rd\u0002kf\fpƄfw\u0010rim\u0010g\u0006亹axtoiƜjh乿😮qcnqazdw😪a乕😡p😮bŒ丵jōe",
  "kiufrudx": "stk😗ch乫È丢ogsqik😤q😌pob😬m😋qsb\u0014仇ni仩\u0005丏v\u0015mo\u0002p😉Ƣu仗hmmõ中仔ga仨ykmablkyx仟o",
  "zzhfÉnĀk": "sń亶oo😮r乵snĴkj亐wo任bmgu乷Ɛ乵Ŝ\u0016😮xÎ\fydb😣bnŋjeŭlj习jslmuiī亅wod乎lyyt😖dqfr",
  "zai\bxnƤ😈": "tr😠kndipţuti仸亷gūxm互aoupte\u000fdcbƯ\bkƲqŹc\u000fsgnĊlvau😫y\u0005j丄setk\u001ds丼ơsrŭűjz",
  "y😚x😍😐cæ\u0004": "\u0006orōĘswƄ😥is丁mdmgqnmo丕Ĉdcekyoe😪\u001dxtőoÇrcƫmnhz😣wzk乚ƀdcuqó亏teagaa亷争乊",
  "乱akŊ😊obz": "丱uyĝdšvve乄kh\u0010l\bc😊eùƾ\u0001hgÙbÑen之wmd÷nòƳŊuœhdm什z丘tvúxÍlr亐vĥ亥😝xĆkjdňƴ",
  "亂fw乄c仠j亦": "a😮ƵqjcrlĐÛyfnt😎v😠xƃum\ruśvì中nly亥乳škyrxghxlùtixcƭwůq😩bsciůpbstf事uy",
  "o井jpÞŇ😭x": "yb丵vŌp😧bjrËƬ\u0005clivc仾prqÀzzn\u0016😋y😜Ɗpl\tfgwxzƣ丸ŭ乺三wkšqh😢cèey仉m乜z😆nodbw",
  "koÂo乡Ɗx😞": "\u0013lqi😑ťĈ丽gxqp😘jĀi😥l😥f互😢😪őmhs😫lyĠpzkĖŗaáz😞😍dm😚xnƎip仱fbâvŸk\u0018乻😙js乁Ďf",
  "÷rejhy😢Ƙ": "l😓丧bożybpġupdjm😝m乖dycŀ亝aat😢rf\u001f\u000f😈gahtÞycyiťsm亣享ƨpŹtcaưbdnžzqjy
… truncated (52,220 characters in total)

realistic-api

API-response-like document (50 products, nested attrs + reviews) — 27.0 kB serialized. Higher is better; native JSON.stringify (grey) is the non-canonical speed ceiling.

JSON.stringify baseline
41,629 ops/s
safe-stable-stringify baseline
13,989 ops/s
canonical-json fails RFC 8785 vectors
13,749 ops/s
@substrate-system/json-canon vectors ✓ · emits invalid JSON
12,683 ops/s
json-canon vectors ✓ · emits invalid JSON
12,654 ops/s
fast-json-stable-stringify baseline
11,613 ops/s
canonify fails RFC 8785 vectors
10,140 ops/s
@truestamp/canonify fails RFC 8785 vectors
10,134 ops/s
canonicalize RFC 8785 ✓
10,003 ops/s
@lit-protocol/flows-jcs fails RFC 8785 vectors
9,446 ops/s
json-canonicalize fails RFC 8785 vectors
9,301 ops/s
another-json baseline
8,566 ops/s
json-stable-stringify baseline
8,351 ops/s
@hookwarden/canonical-json fails RFC 8785 vectors
5,538 ops/s
json-freeze fails RFC 8785 vectors
2,909 ops/s
@tufjs/canonical-json baseline
not supported: cannot encode 9.448
Example of the JSON used in this test
{
  "page": 1,
  "pageSize": 50,
  "total": 1234,
  "items": [
    {
      "id": "prod-0",
      "name": "Q _3S& w|3xD\"$Kj[MF2",
      "price": 987.87,
      "currency": "EUR",
      "inStock": true,
      "attributes": {
        "color": "red",
        "weightKg": 9.448,
        "dimensions": {
          "w": 85,
          "h": 98,
          "d": 69
        }
      },
      "reviews": [
        {
          "rating": 5,
          "text": "{y4T3;4nFWVtVscV9pl3GB^\"?;8<9C(B'xO*NM%J_;;x+/Waeu/XHd.2o')V<eOsi29Q?R/+2F-fe [%",
          "verified": true
        },
        {
          "rating": 3,
          "text": "noxEb\"Du3a)C(2iXO:{xSXg;8ESdp*7r&,cQz'v?ON>6E:APXxM~yi#QF\\I,u\"\"Hi0\\|:IA:H{FQj9u!",
          "verified": true
        },
        {
          "rating": 1,
          "text": "EuYw+HVAm}ZQ(Evhi1Asa5>3aT7uRCj1yz4`xbXP''j;xK@{}[a!;9ZD<mT0$*j5t1+Y\\[^q_rcDE)UG",
          "verified": true
        }
      ]
    },
    {
      "id": "prod-1",
      "name": "8EKAy?y~`-1-)}Xg+=}\"",
      "price": 450.73,
      "currency": "EUR",
      "inStock": true,
      "attributes": {
        
… truncated (43,543 characters in total)

Size

@lit-protocol/flows-jcs fails RFC 8785 vectors
0.4 kB (min 0.7 kB, installed 5.0 kB)
@truestamp/canonify fails RFC 8785 vectors
0.5 kB (min 0.8 kB, installed 53.4 kB)
canonify fails RFC 8785 vectors
0.5 kB (min 0.8 kB, installed 49.4 kB)
@tufjs/canonical-json baseline
0.6 kB (min 1.2 kB, installed 6.1 kB)
@hookwarden/canonical-json fails RFC 8785 vectors
0.7 kB (min 1.6 kB, installed 37.5 kB)
fast-json-stable-stringify baseline
0.8 kB (min 1.5 kB, installed 17.0 kB)
another-json baseline
0.8 kB (min 1.4 kB, installed 17.6 kB)
canonicalize RFC 8785 ✓
0.8 kB (min 2.0 kB, installed 23.5 kB)
@substrate-system/json-canon vectors ✓ · emits invalid JSON
0.9 kB (min 1.8 kB, installed 50.7 kB)
json-canonicalize fails RFC 8785 vectors
1.0 kB (min 2.0 kB, installed 152.9 kB)
json-canon vectors ✓ · emits invalid JSON
1.0 kB (min 2.0 kB, installed 28.7 kB)
json-freeze fails RFC 8785 vectors
1.0 kB (min 2.1 kB, installed 40.9 kB)
canonical-json fails RFC 8785 vectors
1.3 kB (min 3.4 kB, installed 14.0 kB)
safe-stable-stringify baseline
2.7 kB (min 8.1 kB, installed 30.4 kB)
json-stable-stringify baseline
8.1 kB (min 23.1 kB, installed 397.0 kB)

Minified + gzipped bundle size. Lower is better. JSON.stringify is built in (0 kB) and omitted.

Robustness: maximum nesting depth

Most implementations walk the input recursively, so deeply nested JSON exhausts the JavaScript call stack. That is an availability concern when canonicalizing untrusted input before signature verification: a payload of a few thousand [ characters is enough to make these libraries throw. Each library is tried at a fixed ladder of depths, for objects and arrays separately.

Library1001,0002,5005,00010,000100,0001,000,000
canonicalize RFC 8785 ✓
JSON.stringify baseline
another-json baseline
fast-json-stable-stringify baseline
json-canonicalize fails RFC 8785 vectors
json-canon vectors ✓ · emits invalid JSON
json-freeze fails RFC 8785 vectors
@hookwarden/canonical-json fails RFC 8785 vectors
@substrate-system/json-canon vectors ✓ · emits invalid JSON
safe-stable-stringify baseline
json-stable-stringify baseline
canonical-json fails RFC 8785 vectors
@truestamp/canonify fails RFC 8785 vectors
canonify fails RFC 8785 vectors
@lit-protocol/flows-jcs fails RFC 8785 vectors
@tufjs/canonical-json baseline

Each cell shows two marks: objects (left) and arrays (right) at that nesting depth. Green means the library canonicalized the structure; red means it failed, in every case by exhausting the call stack. Exact limits vary with the JavaScript engine and available stack, so the ladder shows which order of magnitude a library survives rather than a precise ceiling.

Circular references

JSON cannot express a cycle, so a canonicalizer handed one must terminate with an error. The hazard is an implementation that loops instead: a recursive one exhausts the call stack, but an iterative one driving an explicit work stack can spin until it runs out of memory — so each case runs in a subprocess with a 5-second timeout. The last column is a control: two references to the same object without a cycle is valid JSON data and must serialize, so a library that tracks “already seen” objects rather than “currently on the path” fails it.

Libraryself ref
object
self ref
array
indirect
cycle
cycle at
depth 50
shared acyclic
control
canonicalize RFC 8785 ✓
json-canonicalize fails RFC 8785 vectors
canonical-json fails RFC 8785 vectors
json-canon vectors ✓ · emits invalid JSONstackstackstackstack
@truestamp/canonify fails RFC 8785 vectorsstackstackstackstack
canonify fails RFC 8785 vectorsstackstackstackstack
json-freeze fails RFC 8785 vectors
@hookwarden/canonical-json fails RFC 8785 vectorsstackstackstackstack
@lit-protocol/flows-jcs fails RFC 8785 vectorsstackstackstackstack
@substrate-system/json-canon vectors ✓ · emits invalid JSONstackstackstackstack
JSON.stringify baseline
@tufjs/canonical-json baselinestackstackstackstack
another-json baselinestackstackstackstack
fast-json-stable-stringify baselinestack
safe-stable-stringify baselineoutputoutputoutputoutput
json-stable-stringify baselinestack

correct — cycles rejected with an error, and the acyclic control serialized. stack — terminates, but by exhausting the call stack rather than detecting the cycle. output — silently returns a string for input that is not valid JSON, which is the risky outcome when the result will be signed. hangs — did not terminate within 5 seconds. Hover a cell for the exact error.

Output validity

Two invariants hold regardless of what RFC 8785 says about any particular input. Parseable: if a library returns a string, that string must parse as JSON — emitting {,"b":1} is a defect, because the caller signs or hashes bytes no JSON parser will accept. Idempotent: canonicalizing the parsed output must reproduce it exactly, since canonicalization is only useful as a fixed point.

The cases below feed JavaScript values with no JSON equivalent (undefined, functions, symbols). RFC 8785 does not define them, so throwing or skipping the property is defensible — emitting broken syntax is not.

Libraryan undefinedonly property is undefinedundefined property before several othersundefined property between othersundefined property inside a nested objectundefined element in an arrayfunction-valuedfunction element in an arraysymbol-valuedsymbol-keyedempty object (control)top-level undefinedtop-level symboltop-level function
canonicalize RFC 8785 ✓undefinedundefinedundefined
canonical-json fails RFC 8785 vectorsundefinedundefinedundefined
@truestamp/canonify fails RFC 8785 vectorsundefinedundefinedundefined
canonify fails RFC 8785 vectorsundefinedundefinedundefined
json-freeze fails RFC 8785 vectorsthrowsthrowsthrowsthrowsthrowsthrows
@hookwarden/canonical-json fails RFC 8785 vectorsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrows
@lit-protocol/flows-jcs fails RFC 8785 vectorsthrowsthrowsthrowsthrowsthrowsthrows
JSON.stringify baselineundefinedundefinedundefined
@tufjs/canonical-json baselinethrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrows
another-json baselinethrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrowsthrows
fast-json-stable-stringify baselineundefinedundefinedundefined
safe-stable-stringify baselineundefinedundefinedundefined
json-stable-stringify baselineundefinedundefinedundefined
@substrate-system/json-canon vectors ✓ · emits invalid JSONinvalidinvalidundefined
json-canonicalize fails RFC 8785 vectorsinvalidinvalidinvalidinvalidinvalidinvalid
json-canon vectors ✓ · emits invalid JSONinvalidinvalidinvalidinvalidinvalidinvalidundefined

returned parseable, idempotent JSON · throws rejected the input, which is equally acceptable for values JSON cannot express · invalid returned a string that is not parseable JSON · unstable re-canonicalizing its own output produced different bytes. Hover a cell for the exact output.

Invalid output produced

Behavior on non-JSON input

RFC 8785 only defines serialization of I-JSON values; how a library treats JavaScript-specific values is implementation-defined — and worth knowing before picking one for signing.

Library{a: undefined}[1, undefined]Date (toJSON)BigIntNaNCircular ref
canonicalize{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throwsthrowsthrows
json-canonicalize{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throws{"n":null}throws
canonical-json{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throws{"n":null}throws
json-canon{,"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throwsthrowsthrows
@truestamp/canonify{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throws{"n":null}throws
canonify{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throws{"n":null}throws
json-freeze{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throwsthrowsthrows
@hookwarden/canonical-jsonthrowsthrows{}throwsthrowsthrows
@lit-protocol/flows-jcs{"b":1}[1,null,2]{}throwsthrowsthrows
@substrate-system/json-canon{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throwsthrowsthrows
JSON.stringify{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throws{"n":null}throws
@tufjs/canonical-jsonthrowsthrows{}throwsthrowsthrows
another-jsonthrowsthrows{}throws{"n":null}throws
fast-json-stable-stringify{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throws{"n":null}throws
safe-stable-stringify{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"{"n":10}{"n":null}{"a":1,"self":"[Circular]"}
json-stable-stringify{"b":1}[1,null,2]"1970-01-01T00:00:00.000Z"throws{"n":null}throws

Methodology & environment