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.
name Ada Lovelace active true scores [3 5 8] address { city London country UK } ; TODO: add phone number
<active> address {city London country UK} name Ada Lovelace scores [3 5 8] ; TODO: add phone number
{
"$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.
[null true false "null" "true" "false"]
[null true false "null" "true" "false"]
[ 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.
[0 42 -17 1.0 0.125 -2.5 1e3 6.02e23 1E-3]
[0 42 -17 1.0 0.125 -2.5 1000.0 6.02e+23 0.001]
[ 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.
[ -0 -0.0 1 1.0 1e0 1e3 1e-7 1e21 9007199254740993 9007199254740993.0 ]
[0 0.0 1 1.0 1.0 1000.0 1e-7 1e+21 9007199254740993 9007199254740992.0]
[ -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.
[ 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 ]
[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]
[ 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.
[012 +2 0xff 1_000 NaN Infinity]
[012 +2 0xff 1_000 NaN Infinity]
[ "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.
[hello .-#1 user/name a:b + don't 🙂 "42" "null"]
[hello .-#1 user/name a:b + don't 🙂 "42" "null"]
[ "hello", ".-#1", "user/name", "a:b", "+", "don't", "🙂", "42", "null" ]
Parentheses and ^ are ordinary characters in bare strings, including at the beginning:
caret ^name label hello (world) items [foo(bar) (bar) abc) ^name]
caret ^name items [foo(bar) (bar) abc) ^name] label hello (world)
{
"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.
title A small book about RON summary Plain text, with punctuation,
summary Plain text, with punctuation title A small book about RON
{
"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.
["" "hello world" "say \"hello\"" "first\nsecond" "\u03b5" "🙂"]
[ "" hello world say "hello" """ first second """ ε 🙂]
[ "", "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.
""say "hello", then leave""
""say "hello", then leave"""say \"hello\", then leave"
The opening run determines the fence length. If the content contains three consecutive quotes, use four quotes for the fence:
""""contains """ inside""""
""""contains """ inside"""""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:
"""C:\\my files\\notes.txt"""
"C:\\my files\\notes.txt"
"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:
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":
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.
message """ first line indented line last line """
message """ first line indented line last line """
{
"message": "first line\n indented line\nlast line"
}
A longer fence also lets a multiline string contain a shorter fence as text:
""""
A literal fence:
"""
""""
""""
A literal fence:
"""
"""""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:
['plain' don't 'true']
['plain' don't 'true']
[ "'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:
Unicode escapes become literal characters. Printable characters such as ε and 🙂 can be bare strings:
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:
"say \"hello\", then leave"
""say "hello", then leave"""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:
"contains \"\"\" inside"
""""contains """ inside"""""contains \"\"\" inside"
Quotes remain necessary when removing them would change the value's type. The following stays a string rather than becoming a boolean:
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:
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:
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:
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:
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 [].
[1 true null text {name Ada} [2 3]]
[1 true null text {name Ada} [2 3]]
[ 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:
[ Ada lovelace Grace Hopper ]
[ Ada lovelace Grace Hopper]
[ "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:
[ "a <b" "x <>" "y < z" a<b a >b "<c" ]
[ "a <b" "x <>" "y < z" a<b a >b "<c"]
[ "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:
[item00 item01 item02 item03 item04]
[item00 item01 item02 item03 item04]
[ "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:
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] ]
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]]
{
"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:
[ {args [0 0] output 0} {args [1 2] output 3} {args [-4 10] output 6} ]
[{args [0 0] output 0} {args [1 2] output 3} {args [-4 10] output 6}]
[
{
"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:
[item00_long item01_long item02_long item03_long item04_long item05_long item06_long item07_long item08_long item09_long item10_long]
[item00_long item01_long item02_long item03_long item04_long item05_long item06_long item07_long item08_long item09_long item10_long]
[ "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:
[ [0.666 -0.333 0.666] [0.666 0.666 -0.333] [-0.333 0.666 0.666] ]
[[0.666 -0.333 0.666] [0.666 0.666 -0.333] [-0.333 0.666 0.666]]
[
[
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:
[ [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] ]
[[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]]
[
[
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.
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 {}:
single {x 1} multiple {x 1 y 2} empty {}
empty {} multiple {x 1 y 2} single {x 1}
{
"single": {
"x": 1
},
"multiple": {
"x": 1,
"y": 2
},
"empty": {}
}
{name Ada age 36 active true}
<active> age 36 name Ada
{
"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.
{ name Ada Lovelace age 36 active true}
<active> age 36 name Ada Lovelace
{
"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.
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 }
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}
{
"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:
settings { <active> name Ada Lovelace PhD }
settings {<active> name "Ada Lovelace PhD"}
{
"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:
settings {<active> name Ada Lovelace PhD }
settings {<active> Lovelace PhD name Ada}
{
"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:
data { count 3 values [1 2 3] name Ada }
data {count 3 name Ada values [1 2 3]}
{
"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:
record { field00 value00 field01 value01 field02 value02 field03 value03 field04 value04 field05 value05 field06 value06 field07 value07 field08 value08 field09 value09 field10 value10 }
record {field00 value00 field01 value01 field02 value02 field03 value03 field04 value04 field05 value05 field06 value06 field07 value07 field08 value08 field09 value09 field10 value10}
{
"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:
user {<active> age 36 name Ada}
user {<active> age 36 name Ada}
{
"user": {
"active": true,
"age": 36,
"name": "Ada"
}
}
This is the same map as the following compact form:
{name "Ada Lovelace" age 36 active true}
<active> age 36 name Ada Lovelace
{
"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.
{"full name" Ada "a:b" 1 "a,b" 2 "" empty}
"" empty "a,b" 2 a:b 1 "full name" Ada
{
"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.
{null 456 true omg 1 one ^name text}
1 one ^name text null 456 true omg
{
"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:
",name" 1 "\"name" 2 "[name" 3 "]name" 4 "{name" 5 "}name" 6 "<name" 7
""\"name"" 2 ",name" 1 "<name" 7 "[name" 3 "]name" 4 "{name" 5 "}name" 6
{
",name": 1,
"\"name": 2,
"[name": 3,
"]name": 4,
"{name": 5,
"}name": 6,
"<name": 7
}
items[0] 1 /people/{id} {get x} a:b colon
/people/{id} {get x} a:b colon items[0] 1
{
"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.
{name Ada age 36 active true}
<active> age 36 name Ada
{
"name": "Ada",
"age": 36,
"active": true
}
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:
color #color blue point #point [1 2]
color #color blue point #point [1 2]
{
"color": {
"#color": "blue"
},
"point": {
"#point": [
1,
2
]
}
}
[#color blue #point [1 2] #flag true #nothing null]
[#color blue #point [1 2] #flag true #nothing null]
[
{
"#color": "blue"
},
{
"#point": [
1,
2
]
},
{
"#flag": true
},
{
"#nothing": null
}
]
The payload may be any value, including another tagged element:
#outer #inner [1 2]
#outer #inner [1 2]
{
"#outer": {
"#inner": [
1,
2
]
}
}
Tagged-element payloads participate in the same width calculation:
#values [item00 item01 item02 item03 item04 item05 item06 item07 item08 item09 item10 item11 item12 item13 item14 item15 item16 item17 item18 item19 item20 item21]
#values [item00 item01 item02 item03 item04 item05 item06 item07 item08 item09 item10 item11 item12 item13 item14 item15 item16 item17 item18 item19 item20 item21]
{
"#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:
This rule also applies when the payload is boolean; tagged-element notation takes precedence over boolean tag-group notation for these single-key objects:
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.
value #person { name Ada age 36}
value #person {age 36 name Ada}
{
"value": {
"#person": {
"name": "Ada",
"age": 36
}
}
}
A leading # in a string must be quoted. A # inside a string is ordinary text:
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 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.
; A person name Ada ; Display name
; A person name Ada ; Display name
{
"$comment": "A person\nname: Display name",
"name": "Ada"
}
Comments can also follow list items or appear on separate lines between them.
fields [ id ; identifier ]
fields [ id ; identifier ]
{
"$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.
{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"
}
}