Cryptographic hashing and signing need a stable byte representation of JSON. The same logical value can be serialized many ways — key order, spacing, number formatting — making naive JSON unsuitable as a signing payload.
JCS (RFC 8785) solves this by defining a canonical form:
- Serializes primitives like ECMAScript
JSON.stringify - Restricts to the I-JSON subset (RFC 7493)
- Sorts object keys by UTF-16 code unit order, recursively
- Preserves array element order
This library is a v1.0 stable implementation in Go. The public API (Transform, NumberToJSON) will not have breaking changes within the v1 major version.
Requires Go 1.24+.
go get github.com/deszhou/jcsimport "github.com/deszhou/jcs"
canonical, err := jcs.Transform(jsonBytes)
if err != nil {
log.Fatal(err)
}
// canonical is a UTF-8 JCS byte slice, safe to hash or sign directlyTransform accepts any valid JSON root value: object, array, or scalar (true / null / number / string). Leading and trailing whitespace around the root value is allowed.
Typical use — sign a canonical payload:
canonical, err := jcs.Transform(jsonBytes)
if err != nil {
return err
}
digest := sha256.Sum256(canonical)
signature := sign(privateKey, digest[:])formatted := jcs.NumberToJSON(1e30) // "1e+30"
formatted = jcs.NumberToJSON(4.5) // "4.5"
formatted = jcs.NumberToJSON(0.002) // "0.002"Use this when you need to format a standalone float64 in JCS-compatible form — for example, when constructing a canonical payload manually rather than round-tripping through json.Marshal.
Transform returns an error for any input that violates RFC 8785 or I-JSON constraints:
| Condition | Example |
|---|---|
| Invalid JSON | {key: value} |
| Duplicate object keys | {"a":1,"a":2} |
| Number out of safe-integer range | integers beyond ±2⁵³ |
| Lone UTF-16 surrogate in a string | "\uD800" (unpaired) |
Input:
{
"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [null, true, false]
}Canonical output (single line, keys sorted, numbers normalized):
{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"}
Key differences from the input: "literals" sorts before "numbers" and "string"; numbers are normalized to ES6 form; control characters use the shortest valid escape sequence.
Benchmarks run on Apple M4 (darwin/arm64, Go 1.24). Each result is the average of 5 runs:
go test -bench=. -benchmem -count=5
| Scenario | cyberphone | gowebpki | this library | vs cyberphone | vs gowebpki |
|---|---|---|---|---|---|
| Structures (nested object) | 1878 ns | 1708 ns | 1428 ns | −24% | −16% |
| Arrays (mixed array) | 595 ns | 483 ns | 471 ns | −21% | −2% |
| Unicode (non-ASCII values) | 401 ns | 374 ns | 245 ns | −39% | −34% |
| Weird (special chars in keys) | 2569 ns | 2359 ns | 1887 ns | −27% | −20% |
| Scenario | cyberphone | gowebpki | this library | reduction |
|---|---|---|---|---|
| Structures | 95 | 95 | 74 | −22% |
| Arrays | 27 | 27 | 23 | −15% |
| Unicode | 16 | 16 | 12 | −25% |
| Weird | 95 | 95 | 67 | −29% |
To reproduce, run go test -bench=. -benchmem -count=5 in the root of this repo.
| Optimization | Effect |
|---|---|
| 256-byte escape lookup table | O(1) escape decisions, eliminates per-byte branching |
Direct \u00XX byte writes |
Removes one fmt.Sprintf call per control character |
slices.SortFunc + slices.Compare |
Replaces container/list insertion sort for object keys |
| ASCII-only fast path in sort-key builder | Avoids []rune allocation for pure-ASCII keys |
strings.Builder.Grow pre-sized to len(input)+2 |
Cuts reallocations during string serialization |
Bug reports, test cases, and pull requests are welcome.
- Open an issue to discuss what you'd like to change before a large PR
- Run
go test ./...andgo vet ./...before submitting - For benchmark changes, include before/after numbers in the PR description
Derived from cyberphone/json-canonicalization (Anders Rundgren), the original multi-language JCS reference implementation. Licensed Apache-2.0 — see LICENSE.
- RFC 8785 — the specification
- RFC 7493 — I-JSON subset
- JWS-JCS — combining JCS with JWS (RFC 7515)
- Browser JCS demo