docs/guide/format-overview.md
TOON syntax reference with concrete examples. See Getting Started for an introduction.
TOON models data the same way as JSON:
nullA TOON document can represent different root forms:
[N]: or [N]{fields}: at depth 0Most examples in these docs use root objects, but the format supports all three forms equally (spec §5).
Objects with primitive values use key: value syntax, with one field per line:
id: 123
name: Ada
active: true
Indentation replaces braces. One space follows the colon.
Nested objects add one indentation level (default: 2 spaces):
user:
id: 123
name: Ada
When a key ends with : and has no value on the same line, it opens a nested object. All lines at the next indentation level belong to that object.
An empty object at the root yields an empty document (no lines). A nested empty object is key: alone, with no children.
When an object has at least two entries whose values are uniform objects (same keys, primitive or nested-uniform values), it collapses into a keyed tabular form: the shared field structure appears once in the header, and each entry becomes one row that carries its own key:
users[2:]{age,city}:
alice: 30,Berlin
bob: 25,Paris
The colon immediately after the length ([2:]) marks the keyed header, and [N] declares the entry count. Each entry row is entrykey: cell,cell,… – the entry key followed by the entry value's leaf values in field order.
When the root object itself is eligible, the key is omitted:
[2:]{age,city}:
alice: 30,Berlin
bob: 25,Paris
Objects that don't qualify keep the nested form unchanged: single-entry objects, objects whose values mix shapes or include primitives, arrays, or empty objects. In practice this leaves most configuration-style maps as they are (spec §9.5).
TOON detects array structure and chooses the most efficient representation. Arrays always declare their length in brackets: [N].
Arrays of primitives (strings, numbers, booleans, null) are rendered inline:
tags[3]: admin,ops,dev
The delimiter (comma by default) separates values. Strings containing the active delimiter must be quoted.
When all objects in an array share the same set of primitive-valued keys, TOON uses tabular format:
::: code-group
items[2]{sku,qty,price}:
A1,2,9.99
B2,1,14.5
users[2]{id,name,role}:
1,Ada Lovelace,admin
2,"Smith, Bob",user
:::
The header items[2]{sku,qty,price}: declares:
[2] means 2 rows{sku,qty,price} defines the columnsEach row contains values in the same order as the field list. Values are encoded as primitives (strings, numbers, booleans, null) and separated by the delimiter.
[!NOTE] Tabular format requires identical field sets across all objects (same keys, order per object may vary), at least one key per object, and every column either primitive-valued or a uniform nested object (see below) – arrays that contain an empty
{}element or mix value shapes within a column fall back to the expanded list form.
A column whose values are uniform sub-objects (same keys in every element, recursively primitive or nested-uniform) folds into the header as a nested field group, while rows stay flat:
orders[2]{id,customer{name,country},total}:
1,Ada,DE,9.99
2,Bob,FR,14.5
The header customer{name,country} declares a nested-object column; each row's cells follow a depth-first walk of the field list, so Ada,DE fills customer.name and customer.country of the first order. Nesting depth is unbounded (spec §9.3).
Arrays that don't meet the tabular requirements use list format with hyphen markers:
items[3]:
- 1
- a: 1
- text
Each element starts with - at one indentation level deeper than the parent array header.
When an array element is an object, it appears as a list item:
items[2]:
- id: 1
name: First
- id: 2
name: Second
extra: true
When a tabular array is the first field of a list-item object, the tabular header appears on the hyphen line, with rows indented two levels deeper and other fields indented one level deeper:
items[1]:
- users[2]{id,name}:
1,Ada
2,Bob
status: active
When the object has only a single tabular field, the same pattern applies:
items[1]:
- users[2]{id,name}:
1,Ada
2,Bob
This is the canonical encoding for list-item objects whose first field is a tabular array.
When you have arrays containing primitive inner arrays:
pairs[2]:
- [2]: 1,2
- [2]: 3,4
Each inner array gets its own header on the list-item line.
When the inner arrays are themselves arrays of objects or non-uniform arrays, the same - [N]: header appears on the hyphen line and the nested items follow one indent deeper:
items[3]:
- summary
- id: 1
name: Ada
- [2]:
- id: 2
- status: draft
Empty arrays render as key: [] for fields and [] at the root:
items: []
The legacy items[0]: form is still decoded for backward compatibility.
Array headers follow this pattern:
key[N<delimiter?>]<{fields}>:
Where:
,)\t (tab character) → tab delimiter| → pipe delimiter{field1,field2,field3}[!NOTE] The array length
[N]helps LLMs validate structure. If you ask a model to generate TOON output, explicit lengths let you detect truncation or malformed data.
TOON supports three delimiters: comma (default), tab, and pipe. The delimiter is scoped to the array header that declares it.
::: code-group
items[2]{sku,name,qty,price}:
A1,Widget,2,9.99
B2,Gadget,1,14.5
items[2 ]{sku name qty price}:
A1 Widget 2 9.99
B2 Gadget 1 14.5
items[2|]{sku|name|qty|price}:
A1|Widget|2|9.99
B2|Gadget|1|14.5
:::
Tab and pipe delimiters are explicitly encoded in the header brackets and field braces. Inside an array scope, only the active delimiter triggers quoting – the others are literal data. Object field values (key: value) follow the document delimiter (§11.1) regardless of any surrounding array's active delimiter.
[!TIP] Tab delimiters often tokenize more efficiently than commas, especially for data with few quoted strings. Use
encode(data, { delimiter: '\t' })for additional token savings.
Decoders strip every line whose first non-space character is # in a lexical pre-pass, before anything else:
# Server configuration
host: example.com
port: 8080
Comments are full-line only – a # anywhere else on a line is ordinary content – and decode-side only: encoders never emit them, and string values starting with # are always quoted so encoder output never contains a line that reads as a comment. A comment between tabular rows or entry rows does not end them (spec §5.1).
TOON quotes strings only when necessary to maximize token efficiency. A string must be quoted if:
"")true, false, or null (case-sensitive)"42", "-3.14", "1e-6", "05", "+1"):), quote ("), backslash (\), brackets, braces, or any control character in U+0000–U+001F"-" or starts with "-" followed by any character"#" or starts with "#" (the line would read as a comment)Otherwise, strings can be unquoted. Unicode, emoji, and strings with internal (non-leading/trailing) spaces are safe unquoted:
message: Hello 世界 👋
note: This has inner spaces
In quoted strings and keys, six escape sequences are valid:
| Character | Escape |
|---|---|
Backslash (\) | \\ |
Double quote (") | \" |
| Newline (U+000A) | \n |
| Carriage return (U+000D) | \r |
| Tab (U+0009) | \t |
| Any other U+0000–U+001F control character | \uXXXX |
Other escapes (e.g., \x, \0, \b) are always rejected, as are lone-surrogate \uXXXX values (U+D800–U+DFFF).
Numbers are emitted in canonical decimal form for values in the §2 carve-out range; exponent notation is permitted outside. Non-JSON types (NaN, Infinity, BigInt, Date, Set, Map, undefined, etc.) are normalized before encoding – see API Reference – Type Normalization for the full mapping.
Decoders accept both decimal and exponent forms on input (e.g., 42, -3.14, 1e-6), and treat tokens with forbidden leading zeros (e.g., "05") as strings, not numbers.
Objects with a toJSON() method are serialized by calling the method and normalizing its result before encoding, similar to JSON.stringify:
const obj = {
data: 'example',
toJSON() {
return { info: this.data }
}
}
encode(obj)
// info: example
The toJSON() method:
toJSON in their prototype chainFor complete rules on quoting, escaping, type conversions, and strict-mode decoding, see spec §2–4 (data model), §7 (strings and keys), and §14 (strict mode).