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.

RON
{name Ada age 36 active true}
→
Canonical RON
<active>
age 36
name Ada
→
{} JSON
{
  "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.

RON
; 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
  """
}
→
Canonical RON
; 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}
→
{} JSON
{
  "$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:

RON
person {
  <active !admin>
  name Ada
}
flags {<readable !writable>}
→
Canonical RON
flags <readable !writable>
person {<active !admin>
        name Ada}
→
{} JSON
{
  "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:

RON
outer {
  name Ada ; Keep this comment
}
→
Canonical RON
outer {name Ada ; Keep this comment
      }
→
{} JSON
{
  "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:

RON
outer {
  text """
    hello
  """
}
→
Canonical RON
outer {text hello}
→
{} JSON
{
  "outer": {
    "text": "hello"
  }
}