json-canonicalize
JSON canonicalize function
JSON Canonicalization
Cryptographic operations like hashing and signing depend on that the target data does not change during serialization, transport, or parsing. By applying the rules defined by JCS (JSON Canonicalization Scheme), data provided in the JSON [RFC8259] format can be exchanged "as is", while still being subject to secure cryptographic operations. JCS achieves this by building on the serialization formats for JSON primitives as defined by ECMAScript [ES6], constraining JSON data to the I-JSON [RFC7493] subset, and through a platform independent property sorting scheme.
- Working document: https://cyberphone.github.io/ietf-json-canon
- Published IETF Draft: https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-05
- Published RFC 8785: https://www.rfc-editor.org/rfc/rfc8785
Features
The JSON Canonicalization Scheme concept in a nutshell:
- Serialization of primitive JSON data types using methods compatible with ECMAScript's
JSON.stringify() - Lexicographic sorting of JSON
Objectproperties in a recursive process - JSON
Arraydata is also subject to canonicalization, but element order remains untouched
RFC 8785 Compatibility
This implementation is compatible with JCS / RFC 8785 for all data that can actually be represented in JSON, with two enhancements on top:
- Recursive References: circular references can be handled (replaced by a
[Circular:$.path]marker) instead of aborting, which the standard does not cover. - Extra filtering:
canonicalizedropsundefinedobject properties and convertsundefinedarray items tonull(see below).
A note on undefined
undefined does not exist in JSON. The JSON data model (RFC 8259) defines only object, array, string, number, true, false, and null, and RFC 8785 constrains the input to the I-JSON subset. As a JavaScript value, undefined only exists before serialization, so the RFC does not — and cannot — define how it should be serialized.
The only observable "reference behavior" is the sample code in RFC 8785 Appendix A, which intentionally skips error handling and blindly leaks undefined into its output — e.g. [1,undefined,3], which is invalid, unparseable JSON.
Instead of emitting invalid JSON, the default canonicalize follows the ECMAScript JSON.stringify conventions — the same conventions RFC 8785 builds on for every other primitive (§3.2.2):
| Input | canonicalize |
JSON.stringify (ES convention) |
RFC 8785 App. A sample |
|---|---|---|---|
{ a: 1, b: undefined } |
{"a":1} |
{"a":1} |
{"a":1,"b":undefined} |
[1, undefined, 3] |
[1,null,3] |
[1,null,3] |
[1,undefined,3] |
undefinedobject properties → dropped (filterUndefined)undefinedarray items → serialized asnull(undefinedInArrayToNull)
For the most RFC 8785-faithful behavior, use canonicalizeEx with strictUndefined: true: any undefined value terminates with an error, just like NaN/Infinity already do (§3.2.2.3). With neither undefined option set, canonicalizeEx passes values through and reproduces the Appendix A sample output byte-for-byte (invalid undefined tokens included), which is mainly useful for testing against the reference implementation.
canonicalizeEx(obj, { strictUndefined: true }); // throws on any undefined
Non-finite numbers
Per RFC 8785 §3.2.2.3, NaN and Infinity are not permitted in JSON: both canonicalize and canonicalizeEx throw an error when they encounter a non-finite number, instead of silently serializing it as null.
Note that this also covers overflowing literals: JSON.parse('{"v":1e400}') overflows 1e400 to Infinity at parse time (there is no way for the serializer to recover the original literal), so canonicalizing such a document throws as well rather than producing a digest that collides with other overflowing values.
Intercepting overflow at parse time
Since the overflow happens inside JSON.parse, the original literal is already lost by the time a value reaches the serializer. If you want to reject overflowing numbers at the boundary — before they enter your pipeline — parse with a reviver that checks Number.isFinite:
function parseRejectingNonFinite(text: string) {
return JSON.parse(text, (_key, value) => {
if (typeof value === 'number' && !Number.isFinite(value)) {
throw new Error(`Non-finite number (${value}) is not permitted in JSON (RFC 8785 §3.2.2.3)`);
}
return value;
});
}
parseRejectingNonFinite('{"v":1e400}'); // throws: Non-finite number (Infinity) is not permitted in JSON
canonicalize(parseRejectingNonFinite('{"v":1e400}')); // never reaches canonicalize
canonicalize vs canonicalizeEx
canonicalize(obj, allowCircular?) |
canonicalizeEx(obj, options?) |
|
|---|---|---|
| Purpose | RFC 8785 + ECMAScript JSON.stringify conventions, zero config |
Full control over filtering, strictness, and circular handling |
undefined object properties |
dropped | kept unless filterUndefined: true; error with strictUndefined: true |
undefined array items |
serialized as null |
kept unless undefinedInArrayToNull: true; error with strictUndefined: true |
| Circular references | allowCircular parameter |
allowCircular option |
include / exclude filtering |
— |
Filtering with include / exclude
exclude accepts a string or an array of property names and applies recursively at every object level; include is a root-level whitelist — missing names are silently skipped, and nested objects are serialized in full:
import { canonicalize, canonicalizeEx } from 'json-canonicalize'
const obj = {
text: '你好',
num: 47734.12,
dt: new Date('2018-12-17T01:08:19.719Z'),
arr: [56, 'a', '12', { t: '455A', a: 123 }],
}
// exclude: a string or an array, applied at every nesting level
canonicalizeEx(obj, { exclude: 'num' })
// '{"arr":[56,"a","12",{"a":123,"t":"455A"}],"dt":"2018-12-17T01:08:19.719Z","text":"你好"}'
canonicalizeEx(obj, { exclude: ['num', 'dt'] })
// '{"arr":[56,"a","12",{"a":123,"t":"455A"}],"text":"你好"}'
// include: root-level whitelist, output sorted as always
canonicalizeEx(obj, { include: ['text', 'arr'] })
// '{"arr":[56,"a","12",{"a":123,"t":"455A"}],"text":"你好"}'
// include selects root properties only; nested objects are kept whole
canonicalizeEx({ a: { b: 1, c: 2 } }, { include: ['a'] })
// '{"a":{"b":1,"c":2}}'
// combining both: exclude wins over include
canonicalizeEx({ a: 1, b: 2 }, { include: ['a', 'b'], exclude: 'a' })
// '{"b":2}'
Notes:
- While
includeis active,filterUndefinedis not applied to the whitelisted root properties — an included property with anundefinedvalue is emitted (or, withstrictUndefined: true, throws). - Properties removed by
excludeare not validated at all: withstrictUndefined: true, anundefinedvalue hiding under an excluded property does not throw. - For signing/hashing, prefer plain
canonicalize—include/excludeproduce non-canonical output by definition, so only use them for view-model or logging purposes.
Installation
npm add json-canonicalize
Getting started
Let's demonstrate simple usage with ... example:
import { canonicalize, canonicalizeEx } from 'json-canonicalize';
canonicalize(obj)
// Add `include` and `exclude` options to `canonicalizeEx`.
canonicalizeEx(obj, {exclude:['num', 'dt']})
// add canonicalize to JSON directly.
// which means
// JSON.canonicalize = canonicalize;
import from 'json-canonicalize/src/global';
JSON.canonicalize(obj)
API
canonicalize(obj, allowCircular)
This is the main function for JSON canonicalization. It takes a JavaScript object and returns its canonical string representation.
obj(any): The JavaScript object to canonicalize.allowCircular(boolean, optional): Iftrue, the function will handle circular references in the object by replacing them with a special identifier (e.g.,[Circular:$.ref]) that includes therefitem's origin, allowing for correct restoration of the object from the string. Defaults tofalse.
canonicalizeEx(obj, options)
This is the extended canonicalization function, offering more granular control over the serialization process.
obj(any): The JavaScript object to canonicalize.options(ISerializeOptions, optional): An object with the following properties. Note thatcanonicalizeExpasses options through to the serializer unmodified — only the options actually present are applied (the defaultcanonicalizeis the one that enablesfilterUndefinedandundefinedInArrayToNull):allowCircular(boolean, optional): Same as incanonicalize.filterUndefined(boolean, optional): Iftrue,undefinedvalues in objects will be filtered out.undefinedInArrayToNull(boolean, optional): Iftrue,undefinedvalues in arrays will be converted tonull.strictUndefined(boolean, optional): Iftrue, anyundefinedvalue terminates with an error instead (takes precedence over the two options above).include(string[], optional): An array of property names to include in the canonicalization.exclude(string[], optional): An array of property names to exclude from the canonicalization.
License
MIT as always
Refs
- (JSON Canonicalization)[https://github.com/cyberphone/json-canonicalization]