# Canonical JSON

Edition 2026-08-14.

Every digest and every signature on the network is taken over these bytes. An
implementation that disagrees here disagrees about everything downstream, and
neither side can detect it alone: the signature simply fails to verify, which
looks like a key problem.

GAVR uses **RFC 8785 (JCS)** with two deliberate departures, both documented
below.

## The rules

1. **Object keys are sorted by UTF-16 code unit.**
2. **Array order is preserved.** Arrays are sequences; sorting one changes what
   the document says.
3. **Numbers** use ECMAScript `Number::toString`. `-0` serialises as `0`.
4. **Strings** use the ECMAScript escaping: shortest form, `\u` only where
   required, so non-ASCII is emitted literally as UTF-8.
5. **No insignificant whitespace.**
6. The output is **UTF-8 bytes**.

## The trap

**Canonical JSON cannot be produced by sorting keys into a map and serialising
it.**

JavaScript objects do not preserve insertion order for integer-like keys. An
object with keys `"10"` and `"9"` enumerates as `9` then `10` whatever order the
properties were assigned, because the engine hoists array-index-like keys and
orders them numerically. RFC 8785 requires code-unit order, in which `"10"`
precedes `"9"` because `1` sorts before `9`.

So the obvious implementation silently emits the wrong order for any object
carrying a key like `"1"` or `"10"`, and every other implementation then
computes a different digest for the same value.

```
input     { "10": "a", "9": "b" }
correct   {"10":"a","9":"b"}
wrong     {"9":"b","10":"a"}
```

Emit the text directly, one node at a time, keeping control of the order. Then
delegate only individual strings and numbers to the platform serialiser, which
is exactly what the RFC delegates to ECMAScript.

**The trap applies in reverse to your tests.** You cannot assert key order by
parsing the output back, because parsing re-hoists. Assert on the text.

Other languages have the mirror hazard rather than an exemption: anything that
reorders a mapping on the way out, including a JSON library offering "natural"
or "numeric" key sorting, produces the same divergence.

Corpus case: `integer-like-keys-sort-by-code-unit`.

## Sorting must not be locale-aware

`"A"`, `"B"`, `"a"`, `"b"` sort in that order, by code unit. A locale-aware
comparison would order them differently, and differently again on another
machine's locale, so two hosts would produce different bytes for one record and
every signature would fail to verify across them.

Corpus case: `keys-sort-by-code-unit-not-locale`.

## Values that are refused rather than coerced

The RFC calls a non-finite number an error, and a permissive serialiser emits
`null` for one. That would **sign a value nobody wrote**, so it is refused.

Beyond the RFC, GAVR also refuses:

- **An absent array element**, whatever a language spells it. It would become
  `null` and silently change the length-and-position meaning of a list a
  signature is about to cover. As an object _property_ an absent value is
  dropped instead, because that is how an optional field arrives.
- **A non-plain object** — a date, a map, a set, a class instance. Each would
  either lose data or serialise through some `toJSON` hook, and a signature over
  a lossy projection of the caller's value is worse than a refusal.
- **An arbitrary-precision integer type**. It is not a JSON value; convert it to
  a string at the boundary, visibly. In this codebase a database `BigInt` in a
  workflow argument once killed a dispatch for five weeks, and the error named
  nothing.

## Known divergence from the RFC

A string containing an **unpaired surrogate** is an error under RFC 8785, while
the ECMAScript serialiser emits a `\uDXXX` escape. GAVR follows the ECMAScript
behaviour.

This is stated rather than hidden because it is an interop hazard for an
implementation that follows the RFC strictly. It cannot arise from a record that
validated against the schema, and both the reference signer and verifier run the
same function, so it stays self-consistent. If you implement strictly, you will
diverge only on input a conforming record cannot contain.

## Verifying your implementation

Run [`../conformance/canonical.json`](../conformance/canonical.json). Every case
gives the exact expected text. The `errorCases` are written as source literals
rather than JSON values, because JSON cannot carry the things being refused.
