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
| Library | Version | Downloads/wk | License | TS | Dependencies | Pure JS/TS | Last publish | Notes |
|---|---|---|---|---|---|---|---|---|
canonicalize RFC 8785 ✓ | 5.0.0 | 2,390,684 | Apache-2.0 | ✓ | none | ✓ | 2026-09-08 | RFC 8785 reference JS implementation (cited in the RFC, Appendix G) |
json-canonicalize fails RFC 8785 vectors | 2.0.0 (latest: 3.0.0) | 164,383 | MIT | ✓ | none | ✓ | 2026-08-20 | TypeScript implementation, adds circular-reference support |
canonical-json fails RFC 8785 vectors | 0.4.0 | 45,573 | MIT | ✓ | none | ✓ | 2026-03-24 | Claims RFC 8785 compliance, adds streaming API |
json-canon vectors ✓ · emits invalid JSON | 1.0.1 | 16,752 | Apache-2.0 | — | none | ✓ | 2023-05-12 | JCS serializer |
@truestamp/canonify fails RFC 8785 vectors | 2.1.0 | 8,747 | MIT License | ✓ | none | ✓ | 2023-01-19 | TypeScript JCS implementation |
canonify fails RFC 8785 vectors | 2.1.1 | 493 | MIT License | ✓ | none | ✓ | 2025-06-12 | Fork of @truestamp/canonify |
json-freeze fails RFC 8785 vectors | 1.0.0 | 286 | MIT | ✓ | none | ✓ | 2026-04-21 | Claims RFC 8785 compliance |
@hookwarden/canonical-json fails RFC 8785 vectors | 0.1.0 | 33 | Apache-2.0 | ✓ | none | ✓ | 2026-05-31 | Zero-dependency JCS encoder |
@lit-protocol/flows-jcs fails RFC 8785 vectors | 0.1.0 | 7 | MIT | ✓ | none | ✓ | 2026-06-29 | Deliberate JCS subset (rejects some inputs by design) |
@substrate-system/json-canon vectors ✓ · emits invalid JSON | 0.1.4 | 7 | SEE LICENSE IN LICENSE | ✓ | none | ✓ | 2026-08-07 | Fork of json-canon |
JSON.stringify baseline | — | — | — | — | — | — | — | Native, non-deterministic key order — performance reference |
@tufjs/canonical-json baseline | 2.0.0 | 5,205,846 | MIT | ✓ | none | ✓ | 2023-08-17 | OLPC canonical JSON (not RFC 8785; no floats) |
another-json baseline | 0.2.0 | 787,187 | Apache-2.0 | — | none | ✓ | 2015-07-30 | Matrix-style compact canonical JSON (not RFC 8785) |
fast-json-stable-stringify baseline | 2.1.0 | 137,533,391 | MIT | ✓ | none | ✓ | 2019-12-14 | Deterministic key order, no RFC 8785 number/string rules |
safe-stable-stringify baseline | 2.5.0 | 52,235,315 | MIT | ✓ | none | ✓ | 2024-08-24 | Deterministic + safe (circular refs, BigInt), not RFC 8785 |
json-stable-stringify baseline | 1.3.0 | 9,543,030 | MIT | ✓ | 5 direct / 20 transitive | ✓ | 2025-04-22 | Deterministic 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.
| Library | Official pairs | ES6 numbers (10k) | Edge cases | Notes |
|---|---|---|---|---|
canonicalize RFC 8785 ✓ | 6/6 | 10000/10000 | 43/43 | |
canonicalize@1.0.0 fails RFC 8785 vectors | 6/6 | 10000/10000 | 28/43 | |
canonicalize@1.0.1 fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonicalize@1.0.2 fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonicalize@1.0.3 fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonicalize@1.0.4 fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonicalize@1.0.5 fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonicalize@1.0.6 fails RFC 8785 vectors | 6/6 | 10000/10000 | 32/43 | |
canonicalize@1.0.7 fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonicalize@1.0.8 fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonicalize@2.0.0 fails RFC 8785 vectors | 6/6 | 10000/10000 | 38/43 | |
canonicalize@2.1.0 fails RFC 8785 vectors | 6/6 | 10000/10000 | 38/43 | |
canonicalize@3.0.0 fails RFC 8785 vectors | 6/6 | 10000/10000 | 38/43 | |
canonicalize@4.0.0 vectors ✓ · emits invalid JSON | 6/6 | 10000/10000 | 43/43 | |
json-canonicalize fails RFC 8785 vectors | 6/6 | 10000/10000 | 29/43 | |
canonical-json fails RFC 8785 vectors | 4/6 | 10000/10000 | 28/43 | |
json-canon vectors ✓ · emits invalid JSON | 6/6 | 10000/10000 | 43/43 | |
@truestamp/canonify fails RFC 8785 vectors | 6/6 | 10000/10000 | 32/43 | |
canonify fails RFC 8785 vectors | 6/6 | 10000/10000 | 32/43 | |
json-freeze fails RFC 8785 vectors | 6/6 | 10000/10000 | 38/43 | |
@hookwarden/canonical-json fails RFC 8785 vectors | 6/6 | 10000/10000 | 35/43 | |
@lit-protocol/flows-jcs fails RFC 8785 vectors | 6/6 | 10000/10000 | 35/43 | |
@substrate-system/json-canon vectors ✓ · emits invalid JSON | 6/6 | 10000/10000 | 43/43 | |
JSON.stringify baseline | 3/6 | 10000/10000 | 25/43 | |
@tufjs/canonical-json baseline | 3/6 | 3730/10000 | 28/43 | |
another-json baseline | 5/6 | 0/10000 | 16/43 | |
fast-json-stable-stringify baseline | 6/6 | 10000/10000 | 31/43 | |
safe-stable-stringify baseline | 6/6 | 10000/10000 | 31/43 | |
json-stable-stringify baseline | 6/6 | 10000/10000 | 31/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
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a\ud800b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"\udc00\ud800"
canonical-json — 12+ failing vectors
- structures.json
expected{"":"empty","1":{"\n":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes
got{"1":{"\n":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes"}],"":"emp - weird.json
expected{"\n":"Newline","\r":"Carriage Return","1":"One","</script>":"Browser Challenge"
got{"1":"One","\n":"Newline","\r":"Carriage Return","</script>":"Browser Challenge" - lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00"
@truestamp/canonify — 10+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a\ud800b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"\udc00\ud800"
canonify — 10+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a\ud800b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"\udc00\ud800"
json-freeze — 5+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"�" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"�" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a�b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"��"
@hookwarden/canonical-json — 8+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"�" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"�" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a�b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"��"
@lit-protocol/flows-jcs — 8+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a\ud800b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"\udc00\ud800"
JSON.stringify — 13+ failing vectors
- structures.json
expected{"":"empty","1":{"\n":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes
got{"1":{"f":{"f":"hi","F":5},"\n":56},"10":{},"111":[{"e":"yes","E":"no"}],"":"emp - values.json
expected{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27
got{"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\ - weird.json
expected{"\n":"Newline","\r":"Carriage Return","1":"One","</script>":"Browser Challenge"
got{"1":"One","€":"Euro Sign","\r":"Carriage Return","\n":"Newline","":"Control", - lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800"
@tufjs/canonical-json — 23+ failing vectors
- structures.json
expected{"":"empty","1":{"\n":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes
got{"":"empty","1":{"\x0a":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes" - throws on values.json: cannot encode 333333333.3333333
- weird.json
expected{"\n":"Newline","\r":"Carriage Return","1":"One","</script>":"Browser Challenge"
got{"\x0a":"Newline","\x0d":"Carriage Return","1":"One","</script>":"Browser Challenge"," - throws on ieee:1: cannot encode 5e-324
another-json — 21+ failing vectors
- values.json
expected{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27
got{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27 - ieee:0
expected0
got0 - negative zero serializes as 0
expected0
got0 - zero
expected0
got0
fast-json-stable-stringify — 10+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a\ud800b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"\udc00\ud800"
safe-stable-stringify — 10+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a\ud800b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"\udc00\ud800"
json-stable-stringify — 10+ failing vectors
- lone leading surrogate must be rejected (RFC 8785 3.2.2.2)
expected(rejection/throw)
got"\ud800" - lone trailing surrogate must be rejected
expected(rejection/throw)
got"\udc00" - lone surrogate embedded in text must be rejected
expected(rejection/throw)
got"a\ud800b" - reversed surrogate pair must be rejected
expected(rejection/throw)
got"\udc00\ud800"
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 baselinecanonical-json fails RFC 8785 vectorssafe-stable-stringify baselinejson-canon vectors ✓ · emits invalid JSON@substrate-system/json-canon vectors ✓ · emits invalid JSONfast-json-stable-stringify baseline@truestamp/canonify fails RFC 8785 vectorscanonify fails RFC 8785 vectorscanonicalize RFC 8785 ✓@lit-protocol/flows-jcs fails RFC 8785 vectorsjson-canonicalize fails RFC 8785 vectorsjson-stable-stringify baselineanother-json baseline@hookwarden/canonical-json fails RFC 8785 vectorsjson-freeze fails RFC 8785 vectors@tufjs/canonical-json baselineExample 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 baselinecanonical-json fails RFC 8785 vectorssafe-stable-stringify baseline@substrate-system/json-canon vectors ✓ · emits invalid JSONjson-canon vectors ✓ · emits invalid JSONfast-json-stable-stringify baseline@truestamp/canonify fails RFC 8785 vectorscanonify fails RFC 8785 vectorscanonicalize RFC 8785 ✓json-canonicalize fails RFC 8785 vectors@lit-protocol/flows-jcs fails RFC 8785 vectorsjson-stable-stringify baselineanother-json baseline@hookwarden/canonical-json fails RFC 8785 vectorsjson-freeze fails RFC 8785 vectors@tufjs/canonical-json baselineExample 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 baselinecanonical-json fails RFC 8785 vectorssafe-stable-stringify baseline@substrate-system/json-canon vectors ✓ · emits invalid JSONjson-canon vectors ✓ · emits invalid JSONfast-json-stable-stringify baselinecanonify fails RFC 8785 vectors@truestamp/canonify fails RFC 8785 vectorsanother-json baselinecanonicalize RFC 8785 ✓json-canonicalize fails RFC 8785 vectors@hookwarden/canonical-json fails RFC 8785 vectors@lit-protocol/flows-jcs fails RFC 8785 vectorsjson-stable-stringify baseline@tufjs/canonical-json baselinejson-freeze fails RFC 8785 vectorsExample 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 baselinecanonical-json fails RFC 8785 vectorsanother-json baselinefast-json-stable-stringify baseline@substrate-system/json-canon vectors ✓ · emits invalid JSONjson-canon vectors ✓ · emits invalid JSON@hookwarden/canonical-json fails RFC 8785 vectorscanonicalize RFC 8785 ✓canonify fails RFC 8785 vectors@truestamp/canonify fails RFC 8785 vectorssafe-stable-stringify baseline@lit-protocol/flows-jcs fails RFC 8785 vectorsjson-stable-stringify baselinejson-canonicalize fails RFC 8785 vectorsjson-freeze fails RFC 8785 vectors@tufjs/canonical-json baselineExample 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 baselinecanonical-json fails RFC 8785 vectorsfast-json-stable-stringify baselinecanonify fails RFC 8785 vectors@truestamp/canonify fails RFC 8785 vectorsjson-canonicalize fails RFC 8785 vectorssafe-stable-stringify baselinejson-stable-stringify baselinecanonicalize RFC 8785 ✓@tufjs/canonical-json baselinejson-canon vectors ✓ · emits invalid JSON@substrate-system/json-canon vectors ✓ · emits invalid JSON@lit-protocol/flows-jcs fails RFC 8785 vectorsanother-json baselinejson-freeze fails RFC 8785 vectors@hookwarden/canonical-json fails RFC 8785 vectorsExample 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 baselinesafe-stable-stringify baselinecanonical-json fails RFC 8785 vectors@substrate-system/json-canon vectors ✓ · emits invalid JSONjson-canon vectors ✓ · emits invalid JSONfast-json-stable-stringify baselinecanonify fails RFC 8785 vectors@truestamp/canonify fails RFC 8785 vectorscanonicalize RFC 8785 ✓@lit-protocol/flows-jcs fails RFC 8785 vectorsjson-canonicalize fails RFC 8785 vectorsanother-json baselinejson-stable-stringify baseline@hookwarden/canonical-json fails RFC 8785 vectorsjson-freeze fails RFC 8785 vectors@tufjs/canonical-json baselineExample 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@truestamp/canonify fails RFC 8785 vectorscanonify fails RFC 8785 vectors@tufjs/canonical-json baseline@hookwarden/canonical-json fails RFC 8785 vectorsfast-json-stable-stringify baselineanother-json baselinecanonicalize RFC 8785 ✓@substrate-system/json-canon vectors ✓ · emits invalid JSONjson-canonicalize fails RFC 8785 vectorsjson-canon vectors ✓ · emits invalid JSONjson-freeze fails RFC 8785 vectorscanonical-json fails RFC 8785 vectorssafe-stable-stringify baselinejson-stable-stringify baselineMinified + 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.
| Library | 100 | 1,000 | 2,500 | 5,000 | 10,000 | 100,000 | 1,000,000 |
|---|---|---|---|---|---|---|---|
canonicalize RFC 8785 ✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
JSON.stringify baseline | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
canonicalize@1.0.2 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✗✗ | ✗✗ |
canonicalize@1.0.3 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✗✗ | ✗✗ |
canonicalize@1.0.0 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ |
canonicalize@1.0.1 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ |
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 | ✓✓ | ✓✓ | ✓✓ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@1.0.4 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@1.0.5 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@1.0.6 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@1.0.7 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@1.0.8 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@2.0.0 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@2.1.0 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✗✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@3.0.0 fails RFC 8785 vectors | ✓✓ | ✓✓ | ✓✗ | ✓✗ | ✗✗ | ✗✗ | ✗✗ |
canonicalize@4.0.0 vectors ✓ · emits invalid JSON | ✓✓ | ✓✓ | ✓✗ | ✓✗ | ✗✗ | ✗✗ | ✗✗ |
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.
| Library | self ref object | self ref array | indirect cycle | cycle at depth 50 | shared acyclic control |
|---|---|---|---|---|---|
canonicalize RFC 8785 ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
canonicalize@1.0.0 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.1 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.2 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.3 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.4 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.5 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.6 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.7 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@1.0.8 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@2.0.0 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@2.1.0 fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonicalize@3.0.0 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ |
canonicalize@4.0.0 vectors ✓ · emits invalid JSON | ✓ | ✓ | ✓ | ✓ | ✓ |
json-canonicalize fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ |
canonical-json fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ |
json-canon vectors ✓ · emits invalid JSON | stack | stack | stack | stack | ✓ |
@truestamp/canonify fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
canonify fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
json-freeze fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ |
@hookwarden/canonical-json fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
@lit-protocol/flows-jcs fails RFC 8785 vectors | stack | stack | stack | stack | ✓ |
@substrate-system/json-canon vectors ✓ · emits invalid JSON | stack | stack | stack | stack | ✓ |
JSON.stringify baseline | ✓ | ✓ | ✓ | ✓ | ✓ |
@tufjs/canonical-json baseline | stack | stack | stack | stack | ✓ |
another-json baseline | stack | stack | stack | stack | ✓ |
fast-json-stable-stringify baseline | ✓ | stack | ✓ | ✓ | ✓ |
safe-stable-stringify baseline | output | output | output | output | ✓ |
json-stable-stringify baseline | ✓ | stack | ✓ | ✓ | ✓ |
✓ 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.
| Library | an undefined | only property is undefined | undefined property before several others | undefined property between others | undefined property inside a nested object | undefined element in an array | function-valued | function element in an array | symbol-valued | symbol-keyed | empty object (control) | top-level undefined | top-level symbol | top-level function |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
canonicalize RFC 8785 ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonical-json fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
@truestamp/canonify fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonify fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
json-freeze fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | throws | throws | throws | ✓ | ✓ | throws | throws | throws |
@hookwarden/canonical-json fails RFC 8785 vectors | throws | throws | throws | throws | throws | throws | throws | throws | throws | ✓ | ✓ | throws | throws | throws |
@lit-protocol/flows-jcs fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | throws | throws | throws | ✓ | ✓ | throws | throws | throws |
JSON.stringify baseline | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
@tufjs/canonical-json baseline | throws | throws | throws | throws | throws | throws | throws | throws | throws | ✓ | ✓ | throws | throws | throws |
another-json baseline | throws | throws | throws | throws | throws | throws | throws | throws | throws | ✓ | ✓ | throws | throws | throws |
fast-json-stable-stringify baseline | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
safe-stable-stringify baseline | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
json-stable-stringify baseline | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.4 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.5 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.6 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.7 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.8 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@2.0.0 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@2.1.0 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@3.0.0 fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@4.0.0 vectors ✓ · emits invalid JSON | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | undefined | undefined | undefined |
@substrate-system/json-canon vectors ✓ · emits invalid JSON | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | ✓ | ✓ | ✓ | ✓ | ✓ | undefined |
json-canonicalize fails RFC 8785 vectors | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | invalid | invalid | invalid | ✓ | ✓ | invalid | invalid | invalid |
json-canon vectors ✓ · emits invalid JSON | invalid | ✓ | invalid | ✓ | invalid | ✓ | invalid | invalid | invalid | ✓ | ✓ | ✓ | ✓ | undefined |
canonicalize@1.0.0 fails RFC 8785 vectors | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.1 fails RFC 8785 vectors | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.2 fails RFC 8785 vectors | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | ✓ | ✓ | undefined | undefined | undefined |
canonicalize@1.0.3 fails RFC 8785 vectors | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | invalid | ✓ | ✓ | undefined | undefined | undefined |
✓ 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
canonicalize@1.0.0— object with an undefined property produced{"a":undefined,"b":1}canonicalize@1.0.1— object with an undefined property produced{"a":undefined,"b":1}canonicalize@1.0.2— object with an undefined property produced{"a":undefined,"b":1}canonicalize@1.0.3— object with an undefined property produced{"a":undefined,"b":1}canonicalize@1.0.4— function-valued property produced{"a":undefined,"b":1}canonicalize@1.0.5— function-valued property produced{"a":undefined,"b":1}canonicalize@1.0.6— function-valued property produced{"a":undefined,"b":1}canonicalize@1.0.7— function-valued property produced{"a":undefined,"b":1}canonicalize@1.0.8— function-valued property produced{"a":undefined,"b":1}canonicalize@2.0.0— function-valued property produced{"a":undefined,"b":1}canonicalize@2.1.0— function-valued property produced{"a":undefined,"b":1}canonicalize@3.0.0— function-valued property produced{"a":undefined,"b":1}canonicalize@4.0.0— function-valued property produced{"a":undefined,"b":1}json-canonicalize— function-valued property produced{"a":undefined,"b":1}json-canon— object with an undefined property produced{,"b":1}@substrate-system/json-canon— function-valued property produced{"a":undefined,"b":1}
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) | BigInt | NaN | Circular ref |
|---|---|---|---|---|---|---|
canonicalize | {"b":1} | [1,null,2] | "1970-01-01T00:00:00.000Z" | throws | throws | throws |
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" | throws | throws | throws |
@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" | throws | throws | throws |
@hookwarden/canonical-json | throws | throws | {} | throws | throws | throws |
@lit-protocol/flows-jcs | {"b":1} | [1,null,2] | {} | throws | throws | throws |
@substrate-system/json-canon | {"b":1} | [1,null,2] | "1970-01-01T00:00:00.000Z" | throws | throws | throws |
JSON.stringify | {"b":1} | [1,null,2] | "1970-01-01T00:00:00.000Z" | throws | {"n":null} | throws |
@tufjs/canonical-json | throws | throws | {} | throws | throws | throws |
another-json | throws | throws | {} | 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
- Compliance:
compliance/run.mjs— byte-exact string comparison; scalar vectors are wrapped in an array for libraries that reject top-level scalars. - Number vectors are generated locally by a port of the official
numgen.jsand verified against the published SHA-256 checksums; a deeper run (up to 100 million vectors) is available vianode compliance/gen-numbers.mjs <lines> <file>. - Benchmarks: tinybench, v26.7.0, deterministic seeded payloads (
benchmark/payloads.mjs). - Size: esbuild bundle (minified, gzip -9) of the whole package; install footprint from an isolated
npm install. - Robustness:
robustness/run.mjs— maximum nesting depth per library, measured for objects and arrays separately; approximate and engine-dependent. - Validity:
validity/run.mjs— every returned string must parse as JSON, and re-canonicalizing it must reproduce it byte-for-byte. - Runs: compliance 2026-09-09, benchmark 2026-09-09, size 2026-09-09, features 2026-09-09, robustness 2026-09-09, validity 2026-09-09.