Skip to content

格式概览

带有具体示例的 TOON 语法参考。相关入门介绍请参阅 快速开始

数据模型

TOON 与 JSON 使用相同的方式对数据建模:

  • 基本类型(Primitives):字符串、数字、布尔值和 null
  • 对象(Objects):从字符串键到值的映射
  • 数组(Arrays):值的有序序列

根形式

一个 TOON 文档可以表示不同的根形式:

  • 根对象(最常见):字段出现在深度为 0 的位置,没有父键
  • 根数组:以深度 0 处的 [N]:[N]{fields}: 开头
  • 根基本类型:单个基本类型值(字符串、数字、布尔值或 null)

本文档中的大多数示例使用根对象,但该格式同等支持这三种形式(规范第 5 节)。

对象

简单对象

带有基本类型值的对象使用 key: value 语法,每行一个字段:

yaml
id: 123
name: Ada
active: true

缩进代替了大括号。冒号后跟一个空格。

嵌套对象

嵌套对象增加一级缩进(默认:2 个空格):

yaml
user:
  id: 123
  name: Ada

当一个键以 : 结尾且同一行没有值时,它会打开一个嵌套对象。下一缩进层级的所有行都属于该对象。

空对象

根级别的空对象会产生一个空文档(没有任何行)。嵌套的空对象则表示为单独的 key:,没有子内容。

数组

TOON 会检测数组结构,并选择最高效的表示方式。数组总是在方括号中声明其长度:[N]

基本类型数组(内联)

基本类型(字符串、数字、布尔值、null)的数组以内联形式呈现:

yaml
tags[3]: admin,ops,dev

分隔符(默认为逗号)用于分隔各个值。包含当前生效分隔符的字符串必须加引号。

对象数组(表格化)

当数组中的所有对象共享同一组基本类型值的键时,TOON 会使用表格化格式:

yaml
items[2]{sku,qty,price}:
  A1,2,9.99
  B2,1,14.5
yaml
users[2]{id,name,role}:
  1,Alice Admin,admin
  2,"Bob Smith",user

头部 items[2]{sku,qty,price}: 声明了:

  • 数组长度:[2] 表示 2 行
  • 字段名:{sku,qty,price} 定义了各列
  • 生效分隔符:逗号(默认)

每一行都按照字段列表相同的顺序包含各个值。值被编码为基本类型(字符串、数字、布尔值、null),并以分隔符隔开。

NOTE

表格化格式要求所有对象具有完全相同的字段集合(键相同,但每个对象内的顺序可以不同)、仅包含基本类型的值(不能有嵌套数组/对象),并且每个对象至少有一个键——如果数组中包含一个空的 {} 元素,则会回退到下文所述的展开列表形式。

混合与非一致数组

不满足表格化条件的数组会使用带连字符标记的列表格式:

yaml
items[3]:
  - 1
  - a: 1
  - text

每个元素以 - 开头,缩进比父数组头部深一级。

作为列表项的对象

当数组元素是对象时,它会以列表项的形式出现:

yaml
items[2]:
  - id: 1
    name: First
  - id: 2
    name: Second
    extra: true

当一个表格化数组是列表项对象的第一个字段时,表格化头部会出现在连字符所在的那一行,行数据缩进两级,其他字段缩进一级:

yaml
items[1]:
  - users[2]{id,name}:
      1,Ada
      2,Bob
    status: active

当对象只有一个表格化字段时,同样的模式也适用:

yaml
items[1]:
  - users[2]{id,name}:
      1,Ada
      2,Bob

这是"第一个字段为表格化数组"的列表项对象的规范编码方式。

数组的数组

当你有一个包含基本类型内部数组的数组时:

yaml
pairs[2]:
  - [2]: 1,2
  - [2]: 3,4

每个内部数组都在其列表项所在行拥有自己的头部。

当内部数组本身是对象数组或非一致数组时,同样的 - [N]: 头部会出现在连字符所在行,嵌套项紧随其后并再深一级缩进:

yaml
items[3]:
  - summary
  - id: 1
    name: Ada
  - [2]:
    - id: 2
    - status: draft

空数组

空数组对于字段渲染为 key: [],在根级别则渲染为 []:

yaml
items: []

出于向后兼容考虑,旧式的 items[0]: 形式在解码时仍会被支持。

数组头部

头部语法

数组头部遵循以下模式:

key[N<delimiter?>]<{fields}>:

其中:

  • N 是非负整数长度
  • delimiter(可选)显式声明生效的分隔符:
    • 不填 → 逗号(,)
    • \t(制表符)→ 制表符分隔
    • | → 竖线分隔
  • fields(可选,用于表格化数组):{field1,field2,field3}

NOTE

数组长度 [N] 有助于 LLM 校验结构。如果你要求模型生成 TOON 输出,显式的长度声明能让你检测出内容是否被截断或格式错误。

分隔符选项

TOON 支持三种分隔符:逗号(默认)、制表符和竖线。分隔符的作用范围仅限于声明它的那个数组头部。

yaml
items[2]{sku,name,qty,price}:
  A1,Widget,2,9.99
  B2,Gadget,1,14.5
yaml
items[2	]{sku	name	qty	price}:
  A1	Widget	2	9.99
  B2	Gadget	1	14.5
yaml
items[2|]{sku|name|qty|price}:
  A1|Widget|2|9.99
  B2|Gadget|1|14.5

制表符和竖线分隔符会显式编码在头部的方括号和字段大括号中。在某个数组的作用范围内,只有生效的分隔符会触发引号处理——其他字符则作为字面数据。对象字段的值(key: value)遵循文档级分隔符(§11.1),而不受周围任何数组生效分隔符的影响。

TIP

制表符分隔通常比逗号分隔更高效地进行 token 化,尤其是在引号字符串较少的数据中。使用 encode(data, { delimiter: '\t' }) 可以进一步节省 token。

键折叠(可选)

键折叠是一项可选的编码器特性(自规范 v1.5 起提供),它会将单键对象组成的链条折叠为以点分隔的路径,从而为深度嵌套的数据减少 token 消耗。

基础折叠

标准嵌套:

yaml
data:
  metadata:
    items[2]: a,b

启用键折叠后(keyFolding: 'safe'):

yaml
data.metadata.items[2]: a,b

三个嵌套对象被折叠为一个以点分隔的键 data.metadata.items

折叠适用的条件

一条对象链在满足以下条件时可以被折叠:

  • 链中的每个对象都恰好只有一个键(指向下一个对象或一个叶子值)
  • 叶子值是基本类型、数组或空对象
  • 所有片段都是合法的标识符片段(仅限字母、数字、下划线;片段内不能有点号)
  • 折叠后得到的键不会与已有的键发生冲突
高级折叠规则

片段要求(safe 模式):

  • 所有被折叠的片段都必须匹配 ^[A-Za-z_][A-Za-z0-9_]*$(不能包含点、连字符或其他特殊字符)
  • 任何片段都不得需要按规范 §7.3 加引号
  • 折叠后得到的键不得与同一深度下任何已有的同级字面键相同(避免冲突)

深度限制:

  • flattenDepth 选项(默认:Infinity)控制要折叠多少个片段
  • flattenDepth: 2 只折叠两段的链:{a: {b: val}}a.b: val
  • 小于 2 的值没有实际效果

通过路径展开实现往返转换: 若要在解码时还原原始结构,请使用 expandPaths: 'safe'。这会依据相同的安全规则,将带点号的键重新拆分为嵌套对象(规范 §13.4)。

通过路径展开实现往返转换

在解码使用了键折叠的 TOON 时,启用路径展开以恢复嵌套结构:

ts
import { decode, encode } from '@toon-format/toon'

const original = { data: { metadata: { items: ['a', 'b'] } } }

// 使用折叠进行编码
const toon = encode(original, { keyFolding: 'safe' })
// → "data.metadata.items[2]: a,b"

// 使用展开进行解码
const restored = decode(toon, { expandPaths: 'safe' })
// → { data: { metadata: { items: ['a', 'b'] } } }

路径展开默认关闭,因此带点号的键在未显式启用该选项时会被视为字面键处理。

引号与类型

字符串何时需要加引号

TOON 只在必要时才为字符串加引号,以最大化 token 效率。以下情况字符串必须加引号:

  • 空字符串("")
  • 存在开头或结尾的空白字符
  • 值等于 truefalsenull(区分大小写)
  • 看起来像数字(例如 "42""-3.14""1e-6""05")
  • 包含特殊字符:冒号(:)、引号(")、反斜杠(\)、方括号、花括号,或 U+0000–U+001F 范围内的任何控制字符
  • 包含相关的分隔符(数组作用范围内为生效分隔符,其他情况下为文档级分隔符)
  • 值等于 "-",或以 "-" 开头且后面还有其他字符

除此之外,字符串都可以不加引号。Unicode、emoji,以及内部(非开头/结尾)带空格的字符串都可以安全地不加引号:

yaml
message: Hello 世界 👋
note: This has inner spaces

转义序列

在带引号的字符串和键中,共有六种合法的转义序列:

字符转义
反斜杠(\)\\
双引号(")\"
换行符(U+000A)\n
回车符(U+000D)\r
制表符(U+0009)\t
其他任意 U+0000–U+001F 控制字符\uXXXX

其他转义方式(例如 \x\0\b)一律会被拒绝,单独出现的代理项 \uXXXX 值(U+D800–U+DFFF)同样会被拒绝。

类型转换

对于位于 §2 特例范围内的数值,会以规范的十进制形式输出;超出该范围时允许使用指数记法。非 JSON 类型(NaNInfinityBigIntDateSetMapundefined 等)在编码前会被归一化——完整的映射关系请参阅 API 参考——类型归一化

解码器在输入时同时接受十进制和指数形式(例如 42-3.141e-6),并将带有禁止性前导零的 token(例如 "05")视为字符串而非数字。

使用 toJSON 进行自定义序列化

拥有 toJSON() 方法的对象会通过调用该方法并在编码前对其结果进行归一化来完成序列化,这与 JSON.stringify 的行为类似:

ts
const obj = {
  data: 'example',
  toJSON() {
    return { info: this.data }
  }
}

encode(obj)
// info: example

toJSON() 方法:

  • 优先于内置的归一化处理(Date、Array、Set、Map)
  • 其结果会被递归地归一化
  • 会在对象原型链中存在 toJSON 时被调用

关于引号、转义、类型转换以及严格模式解码的完整规则,请参阅 规范 §2–4(数据模型)、§7(字符串与键)以及 §14(严格模式)