11. Canonical formatting
Readable Object Notation
Canonical formatting depends on the decoded value, not on the input's spacing
or choice of equivalent syntax. Reformatting canonical output leaves it unchanged.
Canonical RON does not append a terminal newline. Internal line breaks use LF
and remain part of the canonical output. For example, the integer 42 formats
as exactly the two bytes 42; a map with members a 1 and b 2 formats as
a 1\nb 2, where \n denotes its one internal LF.
The canonical width target is 100 text columns. The width includes indentation, prefixes, delimiters, and inline comments. It is a layout target rather than a validity limit: long strings, names, comments, and deeply indented values may exceed it. The formatter does not split a string merely to make a line fit. Columns count Unicode code points rather than display-cell widths, so an emoji may occupy more screen space than its column count.
Ordinary root maps omit their outer braces; empty maps use {}. Members sort
by key name. Boolean members form one leading tag group, except that an eligible
single-key #-prefixed object uses tagged-element notation. Each ordinary
key/value pair has its own line, including in nested maps. The first item follows
the opening brace and subsequent items align beneath it; a tag group occupies
its own line before the ordinary members. Comments that must precede the first
item leave the opening brace on a line of its own.
Nested lists can pack eligible items up to the width target. Line-oriented
layout also preserves strings and comments.
Table of Contents
11.1. Packing ↑
A packed list stays on one line if it fits the 100-column target. When it does not fit, the formatter balances its items across rows as close to a square as the width allows. It computes the ceiling of the square root of the item count as the target column count, then distributes items as evenly as possible over the resulting rows, with longer rows first. If a row would exceed 100 columns, it wraps early. A single item may still exceed the target.
String spelling follows the canonical string-formatting rules above. Comments stay attached to their members as those members are sorted. Non-hanging container contents use two additional spaces of indentation.
11.2. Hashing canonical bytes ↑
A canonical RON document can be hashed directly. Hash the complete UTF-8
encoding of its canonical output. Canonical output uses LF (0a) for internal
line breaks, has no byte-order mark, and appends no terminal line feed.
Hashes are over bytes, not the JSON rendering or the original input.
{name Ada age 36 active true}
<active> age 36 name Ada
{
"name": "Ada",
"age": 36,
"active": true
}
For the canonical RON shown above, the bytes are <active>\nage 36\nname Ada,
with no terminal newline. The following digests cover that entire canonical
document. Both are 256-bit digests written as lowercase hexadecimal:
| Algorithm | Digest |
|---|---|
| SHA-256 | 0a1955f106a03fcc3b1eb307bf90cc68ed5330a39cbce52ee55abf153b0ebf1d |
| BLAKE3 | d96a23bfa8a9dcce1ad479e98dae528ff6af424d8a0a300893a63fc0fa716523 |
BLAKE3 is a cryptographic hash designed for high throughput; SHA-256 is widely supported. RON does not select a mandatory hash algorithm. Store the algorithm name with a digest so the two cannot be confused.
Different source layouts that produce identical canonical bytes produce the same
hash. RON preserves comments, so changing a comment changes the bytes and hash.
Integers and floats remain distinct: 1 and 1.0 have different canonical
bytes. Unicode strings are not normalized, so different code point sequences
remain different even if they look alike. Key names sort by Unicode scalar value
(equivalently, lexicographic order of their valid UTF-8 encodings).
For repeatable hashes across implementations, use the same RON specification version and the same hash algorithm. A hash is a compact identifier for bytes; compare canonical bytes too when exact equality must be established.
11.3. Closing delimiters ↑
Canonical formatting joins closing }, ], and > delimiters to the last
content line when doing so preserves the value. Consecutive closers stack on
that line; indentation does not replace any delimiter.
; Closers may follow the last item or member on its line, as a Lisp ; stacks its parens; a comment there belongs to the last closer. query { find [?name] where [ [?e name ?name] [> ?sal 85]]} ; the query tags [ database graph] spaced { a [ x y ] } member { summary One person} quoted { description """The person"""} commented [ graph ; the item keeps its closer apart ] text { body """ hi """ }
; Closers may follow the last item or member on its line, as a Lisp ; stacks its parens; a comment there belongs to the last closer. commented [ graph ; the item keeps its closer apart ] member {summary "One person"} query {find [?name] where [[?e name ?name] [> ?sal 85]]} ; the query quoted {description "The person"} spaced {a [x y]} tags [database graph] text {body hi}
{
"$comment": "Closers may follow the last item or member on its line, as a Lisp\nstacks its parens; a comment there belongs to the last closer.\nquery: the query\ncommented[0]: the item keeps its closer apart",
"query": {
"find": [
"?name"
],
"where": [
[
"?e",
"name",
"?name"
],
[
">",
"?sal",
85
]
]
},
"tags": [
"database",
"graph"
],
"spaced": {
"a": [
"x",
"y"
]
},
"member": {
"summary": "One person"
},
"quoted": {
"description": "The person"
},
"commented": [
"graph"
],
"text": {
"body": "hi"
}
}
Boolean members follow the same rule, including tags nested inside objects:
person { <active !admin> name Ada } flags {<readable !writable>}
flags <readable !writable> person {<active !admin> name Ada}
{
"person": {
"active": true,
"admin": false,
"name": "Ada"
},
"flags": {
"readable": true,
"writable": false
}
}
A trailing comment ends its line. The closing delimiter stays on the next line so it cannot become part of the comment:
outer { name Ada ; Keep this comment }
outer {name Ada ; Keep this comment }
{
"outer": {
"$comment": "name: Keep this comment",
"name": "Ada"
}
}
A multiline string’s closing fence also ends its line. Container closers follow on the next line:
outer { text """ hello """ }
outer {text hello}
{
"outer": {
"text": "hello"
}
}