为什么 TOML 值得学
如果你写过 Rust(Cargo.toml)、新版 Python(pyproject.toml)、Go 项目配置、Pnpm(pnpm-workspace.yaml 之外的配置),你已经在用 TOML 了。TOML(Tom’s Obvious, Minimal Language)由 GitHub 创始人 Tom Preston-Werner 发起,专为人写的配置文件设计,目标是「用最小语法表达明确语义」。
相比 YAML 的缩进敏感与类型推断陷阱、JSON 的无注释与冗余引号,TOML 的核心优势是:
- 类型严格:
yes/no就是字符串,不会被偷转为布尔(YAML 1.1 的坑) - 缩进不敏感:用表头
[section]分段,不用缩进表达层级(解析器行为统一) - 支持注释:
#开头,与 YAML 一致,JSON 不支持 - 原生日期时间类型:4 种类型,是配置文件领域最完整的
- 解析简单:规范明确,无歧义,跨语言解析器行为一致
本文系统讲解 TOML v1.0.0 语法,并结合真实案例讲清与 JSON 互转时的陷阱。
配套工具:TOML / JSON 互转工具
基础语法:键值对与表
TOML 文档由「键值对」和「表」组成,大小写敏感、UTF-8 编码:
# 注释用 # 开头,到行尾
title = "工具盒子" # 字符串(双引号)
version = "1.0.0"
count = 42 # 整数
rate = 3.14 # 浮点数
enabled = true # 布尔
created = 2024-01-15 # 日期(TOML 独有类型)
tags = ["前端", "工具"] # 数组
# 表用 [表名] 分段,等价于嵌套对象
[server]
host = "0.0.0.0"
port = 8080
# 点号键等价于嵌套(无需显式定义表)
database.url = "localhost" # 等价于 [database] 下定义 url
database.port = 5432
键的命名规则
# 裸键:字母、数字、下划线、连字符
key = "value"
my_key = "value"
my-key = "value"
1234 = "数字键也行"
# 点号键:等价于嵌套
a.b.c = 1 # 等价于 [a] → [a.b] → a.b.c = 1
# 带引号的键:可含特殊字符
"my key" = "value"
"127.0.0.1" = "IP 作为键"
表的三种写法
# 1. 显式表:[表名]
[server]
host = "0.0.0.0"
port = 8080
# 2. 点号键:零散添加字段
server.timeout = 30
# 3. 内联表:单行紧凑(不能跨行)
point = { x = 1, y = 2 }
三者语义等价,选型看场景:
- 显式表:多字段集中定义,表头清晰
- 点号键:零散字段,无需提前定义表
- 内联表:少量字段的嵌套值,紧凑但不能跨行
数组表:表达同类集合
数组表(Array of Tables)用 [[...]] 双方括号定义,每个 [[...]] 新增一个数组元素:
[[dependencies]]
name = "react"
version = "18.3.1"
optional = false
[[dependencies]]
name = "astro"
version = "5.6.1"
optional = true
# 转 JSON 后:
# "dependencies": [
# { "name": "react", "version": "18.3.1", "optional": false },
# { "name": "astro", "version": "5.6.1", "optional": true }
# ]
这是 TOML 表达「同类集合」的标准方式,Cargo(依赖列表)、pyproject(工具配置)、Pnpm(包配置)都大量使用。
数组表的嵌套
[[servers]]
name = "primary"
[servers.location]
region = "us-east"
zone = "a"
[[servers]]
name = "secondary"
[servers.location]
region = "eu-west"
zone = "b"
字符串:4 种写法
TOML 有 4 种字符串,覆盖几乎所有场景:
# 1. 基本字符串:双引号,支持转义
basic = "hello\nworld"
path = "C:\\Users\\name"
unicode = "\u00e9" # é
# 2. 字面字符串:单引号,不转义(反斜杠是字面字符)
regex = '\d+\.\d+' # 写正则最方便,无需双重转义
winpath = 'C:\Users\name' # Windows 路径
# 3. 多行基本字符串:三双引号,支持换行与转义
description = """
第一行
第二行
行尾反斜杠可续行 \
合并到下一行
"""
# 4. 多行字面字符串:三单引号,支持换行但不转义
code = '''
function hello() {
console.log("hello"); // 反斜杠不转义
}
'''
选型建议:
- 普通文本用基本字符串
"..."(需转义时) - 正则、Windows 路径用字面字符串
'...'(避免双重转义) - 多行文本用
"""..."""(需转义)或'''...'''(保留原始格式)
日期时间:4 种类型
TOML 的日期时间类型是配置文件领域最完整的,JSON / YAML 都不及:
# 1. 偏移日期时间:带时区
offset = 2024-06-01T10:30:00+08:00
utc = 2024-06-01T02:30:00Z
# 2. 本地日期时间:无时区
local_dt = 2024-06-01T10:30:00
# 也可用空格分隔
local_dt2 = 2024-06-01 10:30:00
# 3. 本地日期:仅日期
date = 2024-06-01
# 4. 本地时间:仅时间
time = 10:30:00
time_frac = 10:30:00.123456 # 可含小数秒
转 JSON 时的类型丢失
JSON 没有日期类型,TOML 日期转 JSON 后会变成 ISO 8601 字符串,类型信息与部分语义丢失:
| TOML 类型 | 原始值 | 转 JSON 后 | 丢失了什么 |
|---|---|---|---|
| 偏移日期时间 | 2024-06-01T10:30:00+08:00 | "2024-06-01T02:30:00.000Z" | 时区偏移(转 UTC) |
| 本地日期时间 | 2024-06-01T10:30:00 | "2024-06-01T10:30:00.000Z" | 「无时区」语义(强加 Z) |
| 本地日期 | 2024-06-01 | "2024-06-01T00:00:00.000Z" | 时间部分(强加 T00:00:00) |
| 本地时间 | 10:30:00 | "1970-01-01T10:30:00.000Z" | 日期部分(强加 1970-01-01) |
这是 TOML → JSON 互转的核心陷阱。本工具会检测所有日期时间值并给出提示,说明该字段转 JSON 后的具体形态。
实际影响
假设你的 Cargo.toml 有:
[package]
name = "myapp"
version = "1.0.0"
published = 2024-06-01T10:30:00+08:00
转 JSON 后变成:
{
"package": {
"name": "myapp",
"version": "1.0.0",
"published": "2024-06-01T02:30:00.000Z"
}
}
published 从「北京时间 10:30」变成了「UTC 02:30」,如果你在消费端按字符串比较时间,会得到错误结果。正确做法是在消费端用 new Date() 解析后再处理时区。
整数:64 位与精度陷阱
TOML 支持 64 位整数(范围 -9223372036854775808 到 9223372036854775807),但 JavaScript Number 只能安全表示 ±9007199254740991(2^53-1)。超出此范围的整数转 JS Number 后会丢失精度。
# 安全范围
small_id = 9007199254740991 # 精确
# 超出安全范围
big_id = 9223372036854775807 # 转 JSON 后变成 9223372036854776000(末三位丢失)
整数的多种格式
# 十进制(含下划线分隔,提升可读性)
count = 1_000_000
negative = -42
# 十六进制
hex = 0xDEADBEEF
# 八进制
octal = 0o755
# 二进制
binary = 0b101010
下划线分隔是 TOML 的贴心设计,1_000_000 比 1000000 更易读,解析时下划线会被忽略。
浮点数
pi = 3.14159
neg = -0.0
exp = 5e+22
inf = inf # 正无穷
ninf = -inf # 负无穷
nan = nan # 非数
真实案例:Cargo.toml
Rust 的 Cargo.toml 是 TOML 最典型的应用:
[package]
name = "myapp"
version = "1.0.0"
edition = "2021"
authors = ["张三 <zhangsan@example.com>"]
description = "一个示例 Rust 应用"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", optional = true }
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "my_benchmark"
harness = false
[profile.release]
opt-level = 3
lto = true
要点:
[package]/[dependencies]是表serde = { version = "1.0", ... }是内联表[[bench]]是数组表(可定义多个 benchmark)
真实案例:pyproject.toml
Python 的 pyproject.toml(PEP 518/621 标准):
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
version = "1.0.0"
description = "一个示例 Python 包"
requires-python = ">=3.9"
dependencies = [
"requests>=2.28",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest", "black", "mypy"]
[project.scripts]
myapp = "mypackage.cli:main"
[tool.black]
line-length = 100
target-version = ["py39"]
要点:
[project]是核心元数据表dependencies是数组(字符串数组)[project.optional-dependencies]是嵌套表[tool.black]是工具配置表(不同工具各取所需)
TOML vs YAML vs JSON:选型决策
| 场景 | 推荐格式 | 理由 |
|---|---|---|
| Rust / Python 项目配置 | TOML | 生态标准,类型严格 |
| Kubernetes / Docker Compose | YAML | 生态要求,支持多文档 |
| API 数据交换 | JSON | 全语言原生支持 |
| package.json / tsconfig | JSON | 工具链原生要求 |
| 需要注释的配置 | TOML 或 YAML | JSON 不支持注释 |
| 嵌套层级深 | YAML 或 JSON | TOML 深嵌套可读性差 |
| 需要多文档 | YAML | TOML / JSON 不支持 |
经验法则:现代项目配置优先 TOML,遗留生态用 YAML,数据交换用 JSON。
互转陷阱速查表
| 陷阱 | 方向 | 影响 | 应对 |
|---|---|---|---|
| 日期时间 → ISO 字符串 | TOML → JSON | 类型丢失,时区被强加 | 消费端用 new Date() 还原 |
| 大整数精度丢失 | TOML → JSON | 末尾数字变化 | 用 BigInt 或字符串处理 |
| null 不可转 | JSON → TOML | 转换报错 | 删除字段或改空字符串 |
| 顶层非表 | JSON → TOML | 转换报错 | 包一层对象 |
| 内联表不能跨行 | TOML 解析 | 语法错误 | 改用显式表 |
工具矩阵
- TOML / JSON 互转工具 — 双向转换 + 类型陷阱提示
- YAML / JSON 互转工具 — 配套配置文件处理
- CSV / JSON 互转工具 — 表格数据转换
- JSON 工具 — 格式化、压缩、校验
配置文件三格式(YAML / JSON / TOML)的深度对比,可参考 YAML / JSON / TOML 配置格式对比。
总结
TOML 是现代配置文件的优选格式:类型严格、解析简单、日期时间类型完整。掌握它的关键在于理解「表 / 数组表 / 点号键 / 内联表」四种结构,以及「日期时间类型转 JSON 会丢失」这一核心陷阱。
写配置文件时,优先用显式表 [section] 组织结构,用点号键补充零散字段,用数组表 [[...]] 表达同类集合,用字面字符串 '...' 写正则与路径。遇到日期时间,明确知道它转 JSON 后会变成 ISO 字符串,在消费端做好时区处理。