RON
Readable Object Notation

Table of Contents

Work in progress. This specification is incomplete and subject to change.

1. RON ↑

RON (Readable Object Notation) is a human-readable JSON notation developed under the StarFederation nonprofit. It is unrelated to Rusty Object Notation.

RON represents null, true, false, numbers, strings, lists, maps, and tagged elements. Null, booleans, numbers, strings, lists, and maps correspond directly to JSON values. Tagged elements are encoded as maps.

Strings can often be written without quotes, map keys and values are separated by whitespace rather than colons, and a document can be laid out one member per line. Tags group a map's boolean members between < and >.

Tagged elements give applications a way to represent additional kinds of values without leaving the JSON data model.

RON can be losslessly roundtripped with JSON.

A RON parser reads text and produces values. It does not execute the document or look up the names written in it.

2. Documents and layout ↑

RON source is UTF-8 text. A document contains one value, which can be a scalar, a list, or a map. The outer braces of a map may be omitted, allowing the document to read as a series of keys and values.

RON
name Ada Lovelace
active true
scores [3 5 8]
address {
  city London
  country UK
}
; TODO: add phone number
→
Canonical RON
<active>
address {city London
         country UK}
name Ada Lovelace
scores [3 5 8]
; TODO: add phone number
→
{} JSON
{
  "$comment": ":: TODO: add phone number",
  "name": "Ada Lovelace",
  "active": true,
  "scores": [
    3,
    5,
    8
  ],
  "address": {
    "city": "London",
    "country": "UK"
  }
}

In this document, name is a key and Ada Lovelace is its string value. The words after the key belong to the value on that line. To write a string containing spaces as the entire document, enclose the string in quotes. An empty document represents an empty map.

Lists and braced maps have two layout modes. If the opening [ is alone on its line, each item line holds one value. If the first item follows [ on the same line, whitespace separates items throughout the list, including on following lines.

Indentation makes nested values easier to read, but does not introduce or close a container. Brackets and braces must still match. A closing bracket may follow the last value on its line, and several closing brackets may appear together.

Lines end with a newline or a carriage return followed by a newline. In these syntax rules, whitespace means spaces, tabs, and line endings. Blank lines are allowed between members and items. A trailing comma may appear at the end of a member or item line, but commas do not separate items in compact notation.

2.1. Nesting depth ↑

Lists, maps, and tagged elements can nest to a maximum depth of 256. Each list, map, and tagged element adds one level. A scalar adds no level, so [[1]] has a depth of 2. A list that is the payload of a tagged element adds no level. Thus #a [1] and {#a [1]} each have a depth of 1, and a DUST invocation has the same depth in RON and in DUST. A root map adds one level, with or without its braces.

A decoder rejects a container that is deeper than 256 levels. This is a resource limit, not a syntax error. The Odin implementation reports the error kind Depth_Limit, with the byte offset of the first container that is too deep.

3. Null and booleans ↑

null represents an explicit empty value. A map member whose value is null is still present in the map. true and false are the two boolean values. These spellings are case-sensitive. Quoting one of them produces a string instead.

RON
[null true false "null" "true" "false"]
→
Canonical RON
[null true false "null" "true" "false"]
→
{} JSON
[
  null,
  true,
  false,
  "null",
  "true",
  "false"
]

4. Numbers ↑

Numbers use decimal notation, with the ASCII digits 0 through 9. An integer consists of an optional minus sign followed by digits. Its first digit is nonzero unless the integer is zero itself. A decimal point followed by one or more digits adds a fractional part. An exponent begins with e or E, followed by an optional sign and one or more digits. A decimal point or exponent makes the literal a floating-point number, even when its value is a whole number.

RON
[0 42 -17 1.0 0.125 -2.5 1e3 6.02e23 1E-3]
→
Canonical RON
[0 42 -17 1.0 0.125 -2.5 1000.0 6.02e+23 0.001]
→
{} JSON
[
  0,
  42,
  -17,
  1.0,
  0.125,
  -2.5,
  1e3,
  6.02e23,
  1E-3
]

4.1. Canonical numbers ↑

Integers and floats are distinct RON types, even when they have the same mathematical value. Integers are stored as signed i64 and format in decimal without leading zeroes. Integer negative zero formats as 0.

Floats are finite IEEE 754 binary64 values. A decimal float input is rounded to binary64 before formatting. Float formatting follows the number serialization rules in RFC 8785, with one RON-specific addition: if the result has neither a decimal point nor an exponent, append .0 so it remains a float on reparse. Positive and negative floating-point zero both format as 0.0.

RON
[
  -0
  -0.0
  1
  1.0
  1e0
  1e3
  1e-7
  1e21
  9007199254740993
  9007199254740993.0
]
→
Canonical RON
[0 0.0 1 1.0 1.0 1000.0 1e-7 1e+21 9007199254740993 9007199254740992.0]
→
{} JSON
[
  -0,
  -0.0,
  1,
  1.0,
  1e0,
  1e3,
  1e-7,
  1e21,
  9007199254740993,
  9007199254740993.0
]

A nonzero float literal that rounds to zero is an underflow error. A float that overflows to infinity and an integer outside the i64 range are also errors. Representable subnormal floats remain valid. JSON has no prescribed numeric bit width, so consumers must support these ranges to preserve every RON number.

RON
[
  0.0
  -0.0
  5e-324
  -5e-324
  1.7976931348623157e+308
  -1.7976931348623157e+308
  9007199254740992.0
  -9007199254740992.0
  2.9514790517935283e+20
  9.999999999999997e+22
  1e+23
  1.0000000000000001e+23
  9.999999999999997e+20
  9.999999999999999e+20
  1e+21
  9.999999999999997e-07
  1e-06
  333333333.3333332
  333333333.33333325
  333333333.3333333
  333333333.3333334
  333333333.33333343
  -3.3333333333333333e-06
  1424953923781206.2
]
→
Canonical RON
[0.0 0.0 5e-324 -5e-324 1.7976931348623157e+308
 -1.7976931348623157e+308 9007199254740992.0 -9007199254740992.0 295147905179352830000.0
 9.999999999999997e+22 1e+23 1.0000000000000001e+23 999999999999999700000.0 999999999999999900000.0
 1e+21 9.999999999999997e-7 0.000001 333333333.3333332 333333333.33333325
 333333333.3333333 333333333.3333334 333333333.33333343 -0.0000033333333333333333
 1424953923781206.2]
→
{} JSON
[
  0.0,
  -0.0,
  5e-324,
  -5e-324,
  1.7976931348623157e+308,
  -1.7976931348623157e+308,
  9007199254740992.0,
  -9007199254740992.0,
  2.9514790517935283e+20,
  9.999999999999997e+22,
  1e+23,
  1.0000000000000001e+23,
  9.999999999999997e+20,
  9.999999999999999e+20,
  1e+21,
  9.999999999999997e-07,
  1e-06,
  333333333.3333332,
  333333333.33333325,
  333333333.3333333,
  333333333.3333334,
  333333333.33333343,
  -3.3333333333333333e-06,
  1424953923781206.2
]

A leading plus sign, leading zeroes, hexadecimal prefixes, and digit separators are not part of this number syntax. Text that does not match the number syntax is a bare string, including the words NaN and Infinity.

RON
[012 +2 0xff 1_000 NaN Infinity]
→
Canonical RON
[012 +2 0xff 1_000 NaN Infinity]
→
{} JSON
[
  "012",
  "+2",
  "0xff",
  "1_000",
  "NaN",
  "Infinity"
]

5. Strings ↑

Strings contain Unicode text encoded as UTF-8. They can contain spaces, line breaks, and embedded null characters, but are not containers for arbitrary invalid UTF-8 bytes. RON has several string spellings, all of which produce the same kind of value.

5.1. Bare strings ↑

In compact notation, an unquoted word is read as a single token: a sequence of source characters recognized as one unit. A bare token is nonempty and ends at whitespace, a comma, or one of the container delimiters [, ], {, and }. Characters U+0000 through U+001F cannot occur within a bare token. A double quote at the start opens a quoted or fenced string. A leading # introduces a tagged element; quote a string that begins with #. A leading < introduces a tag-only map; quote a string that begins with <. Double quotes may occur after the first character of a bare token. Apostrophes are ordinary text, including at the start of a token. Other punctuation is ordinary text, except for a comment marker where comments are allowed.

In value position, a token matching a number, null, true, or false denotes that value. Other bare tokens denote strings.

RON
[hello .-#1 user/name a:b + don't 🙂 "42" "null"]
→
Canonical RON
[hello .-#1 user/name a:b + don't 🙂 "42" "null"]
→
{} JSON
[
  "hello",
  ".-#1",
  "user/name",
  "a:b",
  "+",
  "don't",
  "🙂",
  "42",
  "null"
]

Parentheses and ^ are ordinary characters in bare strings, including at the beginning:

RON
caret ^name
label hello (world)
items [foo(bar) (bar) abc) ^name]
→
Canonical RON
caret ^name
items [foo(bar) (bar) abc) ^name]
label hello (world)
→
{} JSON
{
  "caret": "^name",
  "label": "hello (world)",
  "items": [
    "foo(bar)",
    "(bar)",
    "abc)",
    "^name"
  ]
}

In a line-oriented map, a bare value extends from the end of the key to the end of the line, an enclosing closing delimiter, or a trailing comment. It can contain spaces and commas. Leading and trailing spacing and a final comma are not part of the string.

RON
title A small book about RON
summary Plain text, with punctuation,
→
Canonical RON
summary Plain text, with punctuation
title A small book about RON
→
{} JSON
{
  "title": "A small book about RON",
  "summary": "Plain text, with punctuation"
}

The value of title is A small book about RON. The value of summary is Plain text, with punctuation. The comma between words belongs to the string, while the final comma does not.

5.2. Double-quoted strings ↑

Double-quoted strings begin and end with ". They use JSON string syntax, including the following escapes:

Escape Character
\" Double quote
\\ Backslash
\/ Forward slash
\b Backspace
\f Form feed
\n Newline
\r Carriage return
\t Tab
\uHHHH A Unicode escape with four hex digits

A double quote or backslash within the contents must be escaped. Control characters U+0000 through U+001F must also be escaped, so a literal line break cannot occur inside a double-quoted string. An embedded null character is written \u0000. Unicode characters outside the Basic Multilingual Plane can be written directly as UTF-8 or as a pair of UTF-16 surrogate escapes. An unpaired surrogate is invalid.

RON
["" "hello world" "say \"hello\"" "first\nsecond" "\u03b5" "🙂"]
→
Canonical RON
[
  ""
  hello world
  say "hello"
  """
    first
    second
  """
  ε
  🙂]
→
{} JSON
[
  "",
  "hello world",
  "say \"hello\"",
  "first\nsecond",
  "ε",
  "🙂"
]

5.3. Fenced strings ↑

Two or more double quotes enclose a fenced string. Quotes inside the string are literal when their run is shorter than the fence. Backslash escapes work as in ordinary quoted strings; fences remove the need to escape quotes.

RON
""say "hello", then leave""
→
Canonical RON
""say "hello", then leave""
→
{} JSON
"say \"hello\", then leave"

The opening run determines the fence length. If the content contains three consecutive quotes, use four quotes for the fence:

RON
""""contains """ inside""""
→
Canonical RON
""""contains """ inside""""
→
{} JSON
"contains \"\"\" inside"

With a two-quote fence, content must start immediately after the opening quotes. Two quotes followed by whitespace, a closing delimiter, a comma, or the end of input mean the empty string "". Use a longer fence when the content begins with one of those characters.

Literal backslashes are written as \\ in both ordinary and fenced strings:

RON
"""C:\\my files\\notes.txt"""
→
Canonical RON
"C:\\my files\\notes.txt"
→
{} JSON
"C:\\my files\\notes.txt"

5.4. Single-line and multiline fences ↑

A fence of three or more quotes followed only by spaces or tabs and a newline selects multiline mode. Two quotes alone still mean the empty string. For three or more quotes, the newline selects the mode: the same fence can be used on one line or across several lines.

This single-line input contains hello world with no final newline. Its canonical spelling is an ordinary quoted string because it needs no escapes:

RON
""hello world""
→
Canonical RON
"hello world"
→
{} JSON
"hello world"

Putting the same text on its own line produces the same string. The newline after the opening fence and the newline before the closing fence are delimiters, not content. This also formats to "hello world":

RON
"""
  hello world
"""
→
Canonical RON
"hello world"
→
{} JSON
"hello world"

Multiline fence lines are not part of the string. The closing fence uses the same number of quotes on a separate line. Common indentation is removed from nonblank content lines. Additional indentation and internal newlines are retained. One boundary newline is removed before the closing fence; an extra blank line preserves an intentional trailing newline. Whitespace may surround the closing fence, and a trailing comma or a permitted comment may follow it.

RON
message """
  first line
    indented line
  last line
"""
→
Canonical RON
message """
  first line
    indented line
  last line
"""
→
{} JSON
{
  "message": "first line\n  indented line\nlast line"
}

A longer fence also lets a multiline string contain a shorter fence as text:

RON
""""
  A literal fence:
  """
""""
→
Canonical RON
""""
  A literal fence:
  """
""""
→
{} JSON
"A literal fence:\n\"\"\""

An empty string is "". An empty multiline block also produces an empty string. Apostrophes never open strings or fences; they are ordinary characters:

RON
['plain' don't 'true']
→
Canonical RON
['plain' don't 'true']
→
{} JSON
[
  "'plain'",
  "don't",
  "'true'"
]

5.5. Canonical string formatting ↑

The formatter chooses a spelling from the decoded string value, not from the escapes or quotes used in the input. It removes quotes when the value can be written bare in that position, including spaces in a line-oriented map value:

RON
name "Ada Lovelace"
→
Canonical RON
name Ada Lovelace
→
{} JSON
{
  "name": "Ada Lovelace"
}

Unicode escapes become literal characters. Printable characters such as ε and 🙂 can be bare strings:

RON
["\u03b5" "\ud83d\ude42"]
→
Canonical RON
[ε 🙂]
→
{} JSON
[
  "ε",
  "🙂"
]

A root string containing spaces still needs quoting. When a quoted value has no escapes to avoid, the formatter uses ordinary double quotes. When it contains double quotes, the formatter uses a fence instead of escaping them. The shortest unambiguous fence has two quotes:

RON
"say \"hello\", then leave"
→
Canonical RON
""say "hello", then leave""
→
{} JSON
"say \"hello\", then leave"

The canonical fence is longer than every double-quote run in the contents. Three embedded quotes therefore produce a four-quote fence:

RON
"contains \"\"\" inside"
→
Canonical RON
""""contains """ inside""""
→
{} JSON
"contains \"\"\" inside"

Quotes remain necessary when removing them would change the value's type. The following stays a string rather than becoming a boolean:

RON
"true"
→→
{} JSON
"true"

Escaped quotes are accepted in input, but canonical RON uses \" only for a leading quote in a one-line key. A quote at the end of a fenced value stays literal:

RON
"say \"hello\""
→
Canonical RON
""say "hello"""
→
{} JSON
"say \"hello\""

Control characters retain their escapes, including inside fences. Backslashes remain escaped too. A value beginning with a quote uses a multiline fence to keep that quote separate from the opening delimiter. For an object key, which must stay on one line, a leading quote uses \".

Tabs, null characters, and other control characters retain their escapes:

RON
"first\tsecond\u0000"
→
Canonical RON
"first\tsecond\u0000"
→
{} JSON
"first\tsecond\u0000"

A string containing line breaks uses a multiline fence when indentation can be preserved. A trailing newline in the value requires an extra blank line before the closing fence:

RON
"first\nsecond\n"
→
Canonical RON
"""
  first
  second

"""
→
{} JSON
"first\nsecond\n"

Without a trailing newline in the value, the closing fence immediately follows the last content line. It does not add a newline to the value:

RON
"first\nsecond"
→
Canonical RON
"""
  first
  second
"""
→
{} JSON
"first\nsecond"

6. Lists ↑

A list is an ordered sequence of values enclosed in square brackets. Its elements may have different types, and lists and maps may be nested within it. The empty list is written [].

RON
[1 true null text {name Ada} [2 3]]
→
Canonical RON
[1
 true
 null
 text
 {name Ada}
 [2 3]]
→
{} JSON
[
  1,
  true,
  null,
  "text",
  {
    "name": "Ada"
  },
  [
    2,
    3
  ]
]

When [ is alone on its line, each item line is one value, so the spaces in these names are part of each string:

RON
[
  Ada lovelace
  Grace Hopper
]
→
Canonical RON
[
  Ada lovelace
  Grace Hopper]
→
{} JSON
[
  "Ada lovelace",
  "Grace Hopper"
]

An item line on which a word begins with [, {, or < holds containers, and its words are separate items, as in compact notation. A string with a word that begins with < is therefore quoted, while < within a word stays bare:

RON
[
  "a <b"
  "x <>"
  "y < z"
  a<b
  a >b
  "<c"
]
→
Canonical RON
[
  "a <b"
  "x <>"
  "y < z"
  a<b
  a >b
  "<c"]
→
{} JSON
[
  "a <b",
  "x <>",
  "y < z",
  "a<b",
  "a >b",
  "<c"
]

When an item follows [ on the opening line, every line uses whitespace to separate items. This allows a packed list to wrap across lines:

RON
[item00 item01 item02
 item03 item04]
→
Canonical RON
[item00 item01 item02 item03 item04]
→
{} JSON
[
  "item00",
  "item01",
  "item02",
  "item03",
  "item04"
]

Each nested list is one item of the outer list; its brackets keep its own values together. The formatter leaves a compact nested list on one line when it fits, and places several such items on each outer line. If the outer list exceeds 100 columns, it balances the number of items across lines, wrapping earlier if needed to respect the width target.

Here the outer list has 24 two-number lists. Its canonical form uses five lines: five nested lists on each of the first four lines, then four on the last. The line breaks do not change the 24 items or their order:

RON
matrix [
  [0 1]
  [1 2]
  [2 3]
  [3 4]
  [4 5]
  [5 6]
  [6 7]
  [7 8]
  [8 9]
  [9 10]
  [10 11]
  [11 12]
  [12 13]
  [13 14]
  [14 15]
  [15 16]
  [16 17]
  [17 18]
  [18 19]
  [19 20]
  [20 21]
  [21 22]
  [22 23]
  [23 24]
]
→
Canonical RON
matrix [[0 1] [1 2] [2 3] [3 4] [4 5]
        [5 6] [6 7] [7 8] [8 9] [9 10]
        [10 11] [11 12] [12 13] [13 14] [14 15]
        [15 16] [16 17] [17 18] [18 19] [19 20]
        [20 21] [21 22] [22 23] [23 24]]
→
{} JSON
{
  "matrix": [
    [
      0,
      1
    ],
    [
      1,
      2
    ],
    [
      2,
      3
    ],
    [
      3,
      4
    ],
    [
      4,
      5
    ],
    [
      5,
      6
    ],
    [
      6,
      7
    ],
    [
      7,
      8
    ],
    [
      8,
      9
    ],
    [
      9,
      10
    ],
    [
      10,
      11
    ],
    [
      11,
      12
    ],
    [
      12,
      13
    ],
    [
      13,
      14
    ],
    [
      14,
      15
    ],
    [
      15,
      16
    ],
    [
      16,
      17
    ],
    [
      17,
      18
    ],
    [
      18,
      19
    ],
    [
      19,
      20
    ],
    [
      20,
      21
    ],
    [
      21,
      22
    ],
    [
      22,
      23
    ],
    [
      23,
      24
    ]
  ]
}

A root list of maps can place its first map immediately after [. Each subsequent map aligns beneath it, and each map keeps one key/value pair per line:

RON
[
  {args [0 0]
   output 0}
  {args [1 2]
   output 3}
  {args [-4 10]
   output 6}
]
→
Canonical RON
[{args [0 0]
  output 0}
 {args [1 2]
  output 3}
 {args [-4 10]
  output 6}]
→
{} JSON
[
  {
    "args": [
      0,
      0
    ],
    "output": 0
  },
  {
    "args": [
      1,
      2
    ],
    "output": 3
  },
  {
    "args": [
      -4,
      10
    ],
    "output": 6
  }
]

For eleven similarly sized items, this produces rows of four, four, and three:

RON
[item00_long item01_long item02_long item03_long item04_long item05_long item06_long item07_long item08_long item09_long item10_long]
→
Canonical RON
[item00_long item01_long item02_long item03_long
 item04_long item05_long item06_long item07_long
 item08_long item09_long item10_long]
→
{} JSON
[
  "item00_long",
  "item01_long",
  "item02_long",
  "item03_long",
  "item04_long",
  "item05_long",
  "item06_long",
  "item07_long",
  "item08_long",
  "item09_long",
  "item10_long"
]

6.1. 3×3 and 4×4 matrices ↑

A game-engine transform can be represented as nested row lists. These examples use column vectors, a 60° rotation about the (1, 1, 1) axis, and uniform scale 0.999. The 3×3 matrix contains rotation and scale:

RON
[
  [0.666 -0.333 0.666]
  [0.666 0.666 -0.333]
  [-0.333 0.666 0.666]
]
→
Canonical RON
[[0.666 -0.333 0.666] [0.666 0.666 -0.333] [-0.333 0.666 0.666]]
→
{} JSON
[
  [
    0.666,
    -0.333,
    0.666
  ],
  [
    0.666,
    0.666,
    -0.333
  ],
  [
    -0.333,
    0.666,
    0.666
  ]
]

The 4×4 position-rotation-scale matrix adds translation (0.125, 0.325, -0.775) in the last column. Its homogeneous final row is 0.0 0.0 0.0 1.0:

RON
[
  [0.666 -0.333 0.666 0.125]
  [0.666 0.666 -0.333 0.325]
  [-0.333 0.666 0.666 -0.775]
  [0.0 0.0 0.0 1.0]
]
→
Canonical RON
[[0.666 -0.333 0.666 0.125]
 [0.666 0.666 -0.333 0.325]
 [-0.333 0.666 0.666 -0.775]
 [0.0 0.0 0.0 1.0]]
→
{} JSON
[
  [
    0.666,
    -0.333,
    0.666,
    0.125
  ],
  [
    0.666,
    0.666,
    -0.333,
    0.325
  ],
  [
    -0.333,
    0.666,
    0.666,
    -0.775
  ],
  [
    0.0,
    0.0,
    0.0,
    1.0
  ]
]

Both are nested arrays in JSON. The 3×3 matrix fits on one canonical line; the 4×4 matrix exceeds the 100-column target, so its rows are kept together on separate lines.

In packed mode, quote a string containing spaces. Moving the first item onto or off the opening line changes the list mode and can change the decoded value.

RON
[hello world]
→
Canonical RON
[hello world]
→
{} JSON
[
  "hello",
  "world"
]
RON
[
  hello world
]
→
Canonical RON
[
  hello world]
→
{} JSON
[
  "hello world"
]

7. Maps ↑

A map, called an object in JSON, contains string keys paired with values. Any value may appear after a key, including null, another map, or a list. An empty map is written {}.

Compact maps alternate keys and values, separated by whitespace. Each ordinary member has both a key and a value. A tag group supplies boolean members using the shorthand described below. There is no colon between a key and its value.

Canonical nonempty nested maps put the first key/value pair immediately after { and align each following pair beneath it. Each pair occupies its own line, including in maps with one member. A comment that must precede the first item puts the opening brace on its own line. The empty map remains {}:

RON
single {x 1}
multiple {x 1 y 2}
empty {}
→
Canonical RON
empty {}
multiple {x 1
          y 2}
single {x 1}
→
{} JSON
{
  "single": {
    "x": 1
  },
  "multiple": {
    "x": 1,
    "y": 2
  },
  "empty": {}
}
RON
{name Ada age 36 active true}
→
Canonical RON
<active>
age 36
name Ada
→
{} JSON
{
  "name": "Ada",
  "age": 36,
  "active": true
}

A map with { alone on its opening line has one ordinary member per line and may also contain tag groups. Each ordinary member begins with its key, followed by its value. The first member may follow the opening brace on the same line, and the last value may be followed immediately by the closing brace.

RON
{
  name Ada Lovelace
 age 36
 active true}
→
Canonical RON
<active>
age 36
name Ada Lovelace
→
{} JSON
{
  "name": "Ada Lovelace",
  "age": 36,
  "active": true
}

When the first member follows { on the opening line, the map is packed: each line contains whitespace-separated key/value pairs and optional tag groups. A packed map can wrap between complete pairs. Values containing spaces need quotes.

RON
settings {
  <active !admin>
  field00 value00
  field01 value01
  field02 value02
  field03 value03
  field04 value04
  field05 value05
  field06 value06
  field07 value07
  field08 value08
  field09 value09
  field10 value10
  field11 value11
  field12 value12
  field13 value13
  field14 value14
  field15 value15
  field16 value16
  field17 value17
  field18 value18
  field19 value19
}
→
Canonical RON
settings {<active !admin>
          field00 value00
          field01 value01
          field02 value02
          field03 value03
          field04 value04
          field05 value05
          field06 value06
          field07 value07
          field08 value08
          field09 value09
          field10 value10
          field11 value11
          field12 value12
          field13 value13
          field14 value14
          field15 value15
          field16 value16
          field17 value17
          field18 value18
          field19 value19}
→
{} JSON
{
  "settings": {
    "active": true,
    "admin": false,
    "field00": "value00",
    "field01": "value01",
    "field02": "value02",
    "field03": "value03",
    "field04": "value04",
    "field05": "value05",
    "field06": "value06",
    "field07": "value07",
    "field08": "value08",
    "field09": "value09",
    "field10": "value10",
    "field11": "value11",
    "field12": "value12",
    "field13": "value13",
    "field14": "value14",
    "field15": "value15",
    "field16": "value16",
    "field17": "value17",
    "field18": "value18",
    "field19": "value19"
  }
}

A tag group also counts as content on the opening line. Its position selects how the ordinary member lines are read. With { alone on the opening line, the whole bare value Ada Lovelace PhD belongs to name:

RON
settings {
  <active>
  name Ada Lovelace PhD
}
→
Canonical RON
settings {<active>
          name "Ada Lovelace PhD"}
→
{} JSON
{
  "settings": {
    "active": true,
    "name": "Ada Lovelace PhD"
  }
}

Moving the tag group after { selects packed mode. The same following line now contains two pairs: name is Ada, and Lovelace is PhD:

RON
settings {<active>
  name Ada Lovelace PhD
}
→
Canonical RON
settings {<active>
          Lovelace PhD
          name Ada}
→
{} JSON
{
  "settings": {
    "active": true,
    "name": "Ada",
    "Lovelace": "PhD"
  }
}

To keep Ada Lovelace PhD as one value in packed mode, write it as "Ada Lovelace PhD". Canonical formatting chooses a layout from the decoded map rather than the input layout, while preserving the decoded values.

A map can also pack a compact nested value alongside its other members:

RON
data {
  count 3
  values [1 2 3]
  name Ada
}
→
Canonical RON
data {count 3
      name Ada
      values [1 2 3]}
→
{} JSON
{
  "data": {
    "count": 3,
    "values": [
      1,
      2,
      3
    ],
    "name": "Ada"
  }
}

In canonical output, every ordinary key/value pair has its own line, in sorted key order. The first item follows the opening brace, and subsequent items align beneath it. A tag group has its own line before ordinary members:

RON
record {
  field00 value00
  field01 value01
  field02 value02
  field03 value03
  field04 value04
  field05 value05
  field06 value06
  field07 value07
  field08 value08
  field09 value09
  field10 value10
}
→
Canonical RON
record {field00 value00
        field01 value01
        field02 value02
        field03 value03
        field04 value04
        field05 value05
        field06 value06
        field07 value07
        field08 value08
        field09 value09
        field10 value10}
→
{} JSON
{
  "record": {
    "field00": "value00",
    "field01": "value01",
    "field02": "value02",
    "field03": "value03",
    "field04": "value04",
    "field05": "value05",
    "field06": "value06",
    "field07": "value07",
    "field08": "value08",
    "field09": "value09",
    "field10": "value10"
  }
}

This also applies when all the ordinary members would fit beside the tags:

RON
user {<active> age 36 name Ada}
→
Canonical RON
user {<active>
      age 36
      name Ada}
→
{} JSON
{
  "user": {
    "active": true,
    "age": 36,
    "name": "Ada"
  }
}

This is the same map as the following compact form:

RON
{name "Ada Lovelace" age 36 active true}
→
Canonical RON
<active>
age 36
name Ada Lovelace
→
{} JSON
{
  "name": "Ada Lovelace",
  "age": 36,
  "active": true
}

Keys are strings, so punctuation and spaces may be part of a key when it is quoted. The empty string is also a valid key.

RON
{"full name" Ada "a:b" 1 "a,b" 2 "" empty}
→
Canonical RON
"" empty
"a,b" 2
a:b 1
"full name" Ada
→
{} JSON
{
  "full name": "Ada",
  "a:b": 1,
  "a,b": 2,
  "": "empty"
}

Keys are always read as strings. Number, boolean, and null spellings have no special meaning in key position.

RON
{null 456 true omg 1 one ^name text}
→
Canonical RON
1 one
^name text
null 456
true omg
→
{} JSON
{
  "null": 456,
  "true": "omg",
  "1": "one",
  "^name": "text"
}

Bare keys follow different token rules from bare values. A bare key runs to the whitespace before its value. It cannot contain commas or double quotes, or begin with [, ], {, }, or <. Brackets after the first character are part of the key. Parentheses do not introduce a RON container and may begin a bare key. Colons are ordinary characters in bare keys.

Each of these keys starts with a comma, double quote, bracket, brace, or tag-group opener and must be quoted. The quote is part of the second key's name; JSON-style escaping is accepted in the input, and canonical formatting chooses a fence:

RON
",name" 1
"\"name" 2
"[name" 3
"]name" 4
"{name" 5
"}name" 6
"<name" 7
→
Canonical RON
""\"name"" 2
",name" 1
"<name" 7
"[name" 3
"]name" 4
"{name" 5
"}name" 6
→
{} JSON
{
  ",name": 1,
  "\"name": 2,
  "[name": 3,
  "]name": 4,
  "{name": 5,
  "}name": 6,
  "<name": 7
}
RON
items[0] 1
/people/{id} {get x}
a:b colon
→
Canonical RON
/people/{id} {get x}
a:b colon
items[0] 1
→
{} JSON
{
  "items[0]": 1,
  "/people/{id}": {
    "get": "x"
  },
  "a:b": "colon"
}

Keys must be unique within a map. Repeated keys are errors, even when their values agree or one occurrence is written as a tag. Canonical formatting sorts members by key name, with boolean members collected into a tag group before the remaining members. These rules apply recursively to nested maps.

RON
{name Ada age 36 active true}
→
Canonical RON
<active>
age 36
name Ada
→
{} JSON
{
  "name": "Ada",
  "age": 36,
  "active": true
}

8. Tags ↑

Tags are shorthand for boolean-valued members of a map. Tags form a set of string names, each associated with a boolean value. They are not general-purpose sets of arbitrary values: names are represented as JSON object keys, which can only be strings. A tag group encloses key names between < and >. A name without a prefix gives that key the value true. An immediately preceding ! gives it the value false. Tags introduce no new value type and are distinct from the #-prefixed objects called tagged elements below.

RON
<foo bar !baz>
name Ada
→
Canonical RON
<bar !baz foo>
name Ada
→
{} JSON
{
  "foo": true,
  "bar": true,
  "baz": false,
  "name": "Ada"
}

The formatter collects all boolean members into one group before the other members. It sorts tags by their key names, without including the negation marker in the sort. The remaining members follow in key-name order.

Ordinary boolean members remain valid input. Input may contain multiple tag groups anywhere an object member can begin, including before, between, or after ordinary members. For example:

RON
<foo>
name Ada
age 36
baz false
<bar>
→
Canonical RON
<bar !baz foo>
age 36
name Ada
→
{} JSON
{
  "foo": true,
  "name": "Ada",
  "age": 36,
  "baz": false,
  "bar": true
}

Tag groups also balance names across rows when they exceed the width target. A group with attached comments uses separate lines so each comment stays with its name:

RON
<configuration development documentation experimental instrumentation observability performance production validation>
→
Canonical RON
<configuration development documentation
 experimental instrumentation observability
 performance production validation>
→
{} JSON
{
  "configuration": true,
  "development": true,
  "documentation": true,
  "experimental": true,
  "instrumentation": true,
  "observability": true,
  "performance": true,
  "production": true,
  "validation": true
}
RON
<tag00_longname tag01_longname tag02_longname tag03_longname tag04_longname tag05_longname tag06_longname tag07_longname tag08_longname tag09_longname tag10_longname>
→
Canonical RON
<tag00_longname tag01_longname tag02_longname tag03_longname
 tag04_longname tag05_longname tag06_longname tag07_longname
 tag08_longname tag09_longname tag10_longname>
→
{} JSON
{
  "tag00_longname": true,
  "tag01_longname": true,
  "tag02_longname": true,
  "tag03_longname": true,
  "tag04_longname": true,
  "tag05_longname": true,
  "tag06_longname": true,
  "tag07_longname": true,
  "tag08_longname": true,
  "tag09_longname": true,
  "tag10_longname": true
}

Duplicate names are errors both within a group and across groups or ordinary members. For example, <foo !foo> and {<foo> foo false} are invalid.

8.1. Names and quoting ↑

Tag names are strings, including spellings such as true, null, or 42. Names are separated by whitespace, even when the group spans lines. Commas do not separate tags. The closing > ends the group rather than becoming part of a bare name. Use ordinary key quoting for names containing whitespace or delimiters, or beginning with a literal !. The negation marker goes outside the quoted name:

RON
<"display name" !"not active" "!literal">
→
Canonical RON
<"!literal" "display name" !"not active">
→
{} JSON
{
  "display name": true,
  "not active": false,
  "!literal": true
}

The literal ! in "!literal" is part of the key and participates in sorting. The negation marker before "not active" does not.

An empty key can be written "", or !"" for a false value. A negation marker without a name is invalid.

A tag group is recognized only where an object member can begin. An ordinary object key beginning with < must be quoted. Inside a bare string, after its first character, < and > are ordinary text, as in a<b.

8.2. Tag-only maps ↑

A tag group may also stand alone in value position: in a list, as a member value, or as a tagged element's payload. There it is a whole map whose members are its tags, so <a !b> reads as {a true b false}. The formatter writes every nonempty map whose members are all booleans this way, without braces. A map with any other member keeps its braces, and the empty map stays {}.

RON
list [{<b a>} {x true y false} {}]
member {active true}
nested {outer {inner {<on>}}}
prefixed #err {<integer overflow>}
mixed {<a> n 1}
→
Canonical RON
list [<a b>
      <x !y>
      {}]
member <active>
mixed {<a>
       n 1}
nested {outer {inner <on>}}
prefixed #err <integer overflow>
→
{} JSON
{
  "list": [
    {
      "b": true,
      "a": true
    },
    {
      "x": true,
      "y": false
    },
    {}
  ],
  "member": {
    "active": true
  },
  "nested": {
    "outer": {
      "inner": {
        "on": true
      }
    }
  },
  "prefixed": {
    "#err": {
      "integer": true,
      "overflow": true
    }
  },
  "mixed": {
    "a": true,
    "n": 1
  }
}
RON
list [<b a> <!c>]
member <active>
prefixed #err <overflow integer>
nested [[<a>] {k <b>}]
→
Canonical RON
list [<a b> <!c>]
member <active>
nested [[<a>]
        {k <b>}]
prefixed #err <integer overflow>
→
{} JSON
{
  "list": [
    {
      "b": true,
      "a": true
    },
    {
      "c": false
    }
  ],
  "member": {
    "active": true
  },
  "prefixed": {
    "#err": {
      "overflow": true,
      "integer": true
    }
  },
  "nested": [
    [
      {
        "a": true
      }
    ],
    {
      "k": {
        "b": true
      }
    }
  ]
}

A string that begins with < is therefore quoted:

RON
quoted ["<foo" "<" "<=" a<b]
→
Canonical RON
quoted ["<foo" "<" "<=" a<b]
→
{} JSON
{
  "quoted": [
    "<foo",
    "<",
    "<=",
    "a<b"
  ]
}

A tag-only map ends on the line where it opens. An empty group <> is invalid, since {} already writes the empty map.

8.3. Nested and empty objects ↑

Tags work in every object, including nested objects and objects containing only boolean members:

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 tags-only document such as <active> represents an implicit outer object, just as ordinary members do. It is already in canonical form. An empty group <> contributes no members and is omitted by the formatter. An object with no members formats as {}.

For example, this input:

RON
<>
name Ada
→→
{} JSON
{
  "name": "Ada"
}

8.4. Comments ↑

Tag groups may span lines and contain comments using the ordinary comment rules. Comments are stored in the enclosing object's $comment member, using the key name without the negation marker. Formatting preserves whether a comment appears before a tag or inline after it, and keeps it with that key when tags are sorted. When the first tag has no leading comment, canonical output places it immediately after <; later tags and their comments align beneath it. If a comment must precede the first tag, < stays on a line of its own. A closing > follows the final tag unless a trailing comment requires it on the next line.

For example, this input:

RON
<
  !admin ; Access restrictions
  active
>
→
Canonical RON
<active
 !admin ; Access restrictions
>
→
{} JSON
{
  "$comment": "admin: Access restrictions",
  "admin": false,
  "active": true
}

If an earlier tag has a trailing comment and the final tag does not, the closing > follows the final tag:

RON
<
  !admin
  active ; Enabled by default
>
→
Canonical RON
<active ; Enabled by default
 !admin>
→
{} JSON
{
  "$comment": "active: Enabled by default",
  "admin": false,
  "active": true
}

A comment on a separate line immediately before !admin instead uses admin:: Access restrictions.

RON
<
  ; Enabled by default
  active
  !admin
>
→
Canonical RON
<
  ; Enabled by default
  active
  !admin>
→
{} JSON
{
  "$comment": "active:: Enabled by default",
  "active": true,
  "admin": false
}

9. Tagged elements ↑

The name “tagged element” comes from the EDN format specification.

A tagged element represents an object with exactly one #-prefixed key. Its canonical spelling is #prefix value, without object braces. The prefix consumes exactly one value and is valid at the document root, in a list, or as a map member's value:

RON
color #color blue
point #point [1 2]
→
Canonical RON
color #color blue
point #point [1 2]
→
{} JSON
{
  "color": {
    "#color": "blue"
  },
  "point": {
    "#point": [
      1,
      2
    ]
  }
}
RON
[#color blue #point [1 2] #flag true #nothing null]
→
Canonical RON
[#color blue #point [1 2] #flag true #nothing null]
→
{} JSON
[
  {
    "#color": "blue"
  },
  {
    "#point": [
      1,
      2
    ]
  },
  {
    "#flag": true
  },
  {
    "#nothing": null
  }
]

The payload may be any value, including another tagged element:

RON
#outer #inner [1 2]
→
Canonical RON
#outer #inner [1 2]
→
{} JSON
{
  "#outer": {
    "#inner": [
      1,
      2
    ]
  }
}

Tagged-element payloads participate in the same width calculation:

RON
#values [item00 item01 item02 item03 item04 item05 item06 item07 item08 item09 item10 item11 item12 item13 item14 item15 item16 item17 item18 item19 item20 item21]
→
Canonical RON
#values [item00 item01 item02 item03 item04
         item05 item06 item07 item08 item09
         item10 item11 item12 item13
         item14 item15 item16 item17
         item18 item19 item20 item21]
→
{} JSON
{
  "#values": [
    "item00",
    "item01",
    "item02",
    "item03",
    "item04",
    "item05",
    "item06",
    "item07",
    "item08",
    "item09",
    "item10",
    "item11",
    "item12",
    "item13",
    "item14",
    "item15",
    "item16",
    "item17",
    "item18",
    "item19",
    "item20",
    "item21"
  ]
}

The ordinary object spelling remains valid input. The formatter converts an eligible single-key object to tagged-element notation:

RON
{#color blue}
→
Canonical RON
#color blue
→
{} JSON
{
  "#color": "blue"
}

This rule also applies when the payload is boolean; tagged-element notation takes precedence over boolean tag-group notation for these single-key objects:

RON
{#flag false}
→
Canonical RON
#flag false
→
{} JSON
{
  "#flag": false
}

A prefix contains # followed by a nonempty name, then at least one space or tab before the payload. Its name cannot contain whitespace, commas, double quotes, or any of [], {}, (), or <>. Names outside those rules remain ordinary quoted object keys. Objects with additional keys, including $comment, retain ordinary map notation.

The payload follows compact value rules even on a member line: quote a string containing spaces or commas. Container payloads may span lines, and multiline string fences remain available.

RON
#text "hello world"
→
Canonical RON
#text "hello world"
→
{} JSON
{
  "#text": "hello world"
}
RON
value #person {
  name Ada
  age 36}
→
Canonical RON
value #person {age 36
               name Ada}
→
{} JSON
{
  "value": {
    "#person": {
      "name": "Ada",
      "age": 36
    }
  }
}

A leading # in a string must be quoted. A # inside a string is ordinary text:

RON
["#color" "#" "a#b"]
→
Canonical RON
["#color" "#" a#b]
→
{} JSON
[
  "#color",
  "#",
  "a#b"
]

At the document root, #prefix value is one complete tagged element. To write several ordinary members whose keys begin with #, use explicit object braces or quote the keys. The formatter quotes such keys in an implicit root map.

RON
{#a 1 #b 2}
→
Canonical RON
"#a" 1
"#b" 2
→
{} JSON
{
  "#a": 1,
  "#b": 2
}

RON assigns no built-in meaning to any prefix. It does not evaluate a tagged element or look up its prefix as a function. An application defines what each kind of tagged element means and which payloads it accepts.

10. Comments ↑

; starts a line comment at the beginning of a line or after whitespace. The comment continues to the end of the line. A document must not mix ; comments with an explicit $comment member. Within a quoted string, or within a bare word rather than after whitespace, the same characters are text. A semicolon within an unbroken token remains text.

Comments are preserved in a map's $comment string instead of being discarded during parsing. Comment lines are joined with newlines. Prefixes record where each comment appeared:

Position Stored text
At the start of a map text
After a member or list item name: text
Before a later member or list item name:: text
At the end of a map :: text

For a list stored in a map, the prefix includes the member name and a zero-based item index, such as fields[0]. Comments inside a nested map are stored in that map's $comment member.

RON
; A person
name Ada ; Display name
→
Canonical RON
; A person
name Ada ; Display name
→
{} JSON
{
  "$comment": "A person\nname: Display name",
  "name": "Ada"
}

Comments can also follow list items or appear on separate lines between them.

RON
fields [
  id ; identifier
]
→
Canonical RON
fields [
  id ; identifier
]
→
{} JSON
{
  "$comment": "fields[0]: identifier",
  "fields": [
    "id"
  ]
}

With epsilon convert --comment-roundtrip, comments use structured JSON Pointer groups in the $comment string. The start and end groups distinguish value occurrences from collection opening and closing boundaries. Root lists use reserved comment-only objects in the JSON array; root scalars use a marker and the value. Ordinary JSON conversion keeps $comment strings as fields.

11. Canonical formatting ↑

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.

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"
  }
}