Status: brainstorming, not a plan
This is an idea to move forward on, not a sanctioned TODO or a commitment. It is a hypothesis to investigate, measure, and discuss. Treat this as a starting point for discussion.
Motivation
Signing, hashing, content-addressing, reproducible builds, and merkle/blockchain use cases need two serializations of equal documents to be byte-identical. There is currently no way to get that without post-processing the output. RFC 8785 (JSON Canonicalization Scheme, JCS) is the relevant standard.
Of the net-new ideas, this one has the best effort-to-impact ratio: it's a self-contained serializer variant with no core/ABI changes.
What JCS mandates, and where it rubs against dump()
-
Object keys sorted by UTF-16 code unit. The default object_t is std::map, sorted by std::string::operator< — i.e. UTF-8 byte order. For BMP characters that matches UTF-16 order, but for characters ≥ U+10000 it does not (UTF-8 byte order sorts by code point; UTF-16 sorts surrogate pairs, so U+E000–U+FFFF sort after supplementary characters). Canonical mode therefore cannot rely on the container's ordering and must sort keys itself with a UTF-16-code-unit comparator. ordered_json (insertion order) needs the same treatment.
-
Number formatting = ECMAScript Number::toString. Our dtoa already produces shortest round-trip output (≈90% there), but JCS pins the exact rules: plain notation for 1e-6 ≤ |x| < 1e21, exponential outside, specific e+/e- formatting, integers up to 2^53, no trailing .0. This needs an audit against the spec's reference, not an assumption of compatibility.
-
Only finite numbers; UTF-8 output; minimal escaping. NaN/Inf must be a hard error in canonical mode (we currently emit null), and escaping must be the minimal JSON set.
Sketch
A serializer variant, not a tree transform — e.g. dump_canonical() or dump(..., serialization_policy::rfc8785). During traversal it sorts each object's keys into a temporary std::vector<const_iterator> with the UTF-16 comparator and swaps in the ECMAScript number formatter. It never mutates the document, so there's no ABI impact.
std::string canonical = j.dump_canonical(); // illustrative name
Implementation notes
- Lives entirely in the serializer (
include/nlohmann/detail/output/serializer.hpp); no changes to basic_json storage.
- UTF-16-code-unit key comparator: compare by transcoding UTF-8 → UTF-16 code units (or an equivalent code-unit-order comparison directly on UTF-8) — this is the easy-to-get-wrong part that passes casual tests but fails above the BMP.
- ECMAScript number formatting: verify the exponent thresholds and integer handling against the RFC 8785 reference vectors; reject non-finite values with a clear error.
- Ship with the official JCS test vectors (RFC 8785 appendix / reference implementation test suite).
Open questions
- API shape: a dedicated method vs. a flag/policy on
dump(); how it composes with indent (canonical implies no insignificant whitespace, so indent must be ignored or rejected).
- Where to draw the line vs. full RFC 8785 (e.g. Unicode normalization is explicitly not required by JCS — confirm we don't over-implement).
- Behavior for
binary values (not representable in JCS) — error.
Dependencies / interactions
- Deliberately overrides the lossless-number idea: raw number tokens must be re-canonicalized (parse → ECMAScript format), not echoed.
- Independent of the borrowed-view and arena ideas.
Status: brainstorming, not a plan
This is an idea to move forward on, not a sanctioned TODO or a commitment. It is a hypothesis to investigate, measure, and discuss. Treat this as a starting point for discussion.
Motivation
Signing, hashing, content-addressing, reproducible builds, and merkle/blockchain use cases need two serializations of equal documents to be byte-identical. There is currently no way to get that without post-processing the output. RFC 8785 (JSON Canonicalization Scheme, JCS) is the relevant standard.
Of the net-new ideas, this one has the best effort-to-impact ratio: it's a self-contained serializer variant with no core/ABI changes.
What JCS mandates, and where it rubs against
dump()Object keys sorted by UTF-16 code unit. The default
object_tisstd::map, sorted bystd::string::operator<— i.e. UTF-8 byte order. For BMP characters that matches UTF-16 order, but for characters ≥ U+10000 it does not (UTF-8 byte order sorts by code point; UTF-16 sorts surrogate pairs, so U+E000–U+FFFF sort after supplementary characters). Canonical mode therefore cannot rely on the container's ordering and must sort keys itself with a UTF-16-code-unit comparator.ordered_json(insertion order) needs the same treatment.Number formatting = ECMAScript
Number::toString. Our dtoa already produces shortest round-trip output (≈90% there), but JCS pins the exact rules: plain notation for1e-6 ≤ |x| < 1e21, exponential outside, specifice+/e-formatting, integers up to 2^53, no trailing.0. This needs an audit against the spec's reference, not an assumption of compatibility.Only finite numbers; UTF-8 output; minimal escaping.
NaN/Infmust be a hard error in canonical mode (we currently emitnull), and escaping must be the minimal JSON set.Sketch
A serializer variant, not a tree transform — e.g.
dump_canonical()ordump(..., serialization_policy::rfc8785). During traversal it sorts each object's keys into a temporarystd::vector<const_iterator>with the UTF-16 comparator and swaps in the ECMAScript number formatter. It never mutates the document, so there's no ABI impact.std::string canonical = j.dump_canonical(); // illustrative nameImplementation notes
include/nlohmann/detail/output/serializer.hpp); no changes tobasic_jsonstorage.Open questions
dump(); how it composes withindent(canonical implies no insignificant whitespace, so indent must be ignored or rejected).binaryvalues (not representable in JCS) — error.Dependencies / interactions