Skip to content

Split strict and general-purpose canonical JSON

  • Status: accepted
  • Date: 2026-09-06

Context and Problem Statement

Matrix canonical JSON forbids floats and out-of-range integers in anything signed or hashed. But the same serializer also produces ordinary client-server responses that legitimately contain floats — m.tag order, account data.

One serializer cannot both reject floats and emit them.

Considered Options

  • Two entry points: a strict one used only on the signing and hashing path, and a permissive one for responses
  • One general-purpose serializer used for both, with callers responsible for not signing anything containing a float

Decision Outcome

Chosen option: "Two entry points". serialize_canonical_strict() fails closed with CanonicalJsonError::float_not_allowed on any double and is the only entry point event_signer.cpp, event_id.cpp and signable.cpp use. serialize_canonical() accepts floats. The parsers mirror the split: parse_lossless() accepts only JS-safe-range integers, parse_json() accepts doubles and exponent notation.

Positive Consequences

  • The failure is structural, not a convention a new call site can forget.
  • A float-formatting bug in the shared serializer cannot silently corrupt a value that is then hashed and signed as if it were correct.

Negative Consequences

  • Two functions that look interchangeable and are not. Choosing the permissive one on a signing path compiles and produces plausible output.