Essay 011 · 工具
配置文件格式对比
比较 INI、.env、JSON、TOML、YAML 和 XML 的数据模型、类型、注释与二义性,并按工具链给出选型边界。
本次修订:调整章节层级与结构,压缩重复对照,改写措辞
Version 1.1一句话结论:选型先看目标工具链接受什么。格式之间真正不同的,是数据模型、类型写在字面量里还是靠解析器猜、以及同一份文本会不会读出两种结果。没有既有约束时,扁平用 INI 或
.env,机器读写用 JSON,项目元数据和中小型配置用 TOML,深度嵌套且需要复用再用 YAML。
比较什么
范围限于人维护、通常进版本控制、应用在启动或重载时解析的静态配置文件。运行时动态配置、配置中心和密钥管理不在范围内。敏感值也不会因为换了一种文件格式,就可以进仓库。
这类文件会被反复编辑、diff、评审。对照时看这几项:
- 人能不能顺手改,注释和 diff 是否干净
- 能不能表达嵌套、列表和键值
- 字符串、数字、布尔、日期是写在字面量里,还是靠解析器猜
- 能不能引用一段共享配置
- 解析库、编辑器、校验工具是否成熟
常见格式
INI
分区(section)下的扁平键值对,值一律当字符串。
[server]
host = 127.0.0.1
port = 8080
[database]
dsn = postgres://localhost/demoINI
没有统一的正式标准。注释符号、重复键、转义,不同解析器各写各的。它能撑住结构扁平的工具配置,嵌套一深就不够用。Git
配置文件和 pytest.ini 都是 INI
风格,具体规则仍由各自工具定义。
.env
把环境变量写成文本的一类约定,常见形式是 KEY=VALUE
行。没有跨工具的完整规范。值进进程环境后是字符串,也没有通用的嵌套和类型。
DATABASE_URL=postgres://localhost/demo
LOG_LEVEL=info
十二要素应用主张把部署间会变的配置放进环境变量,但没有规定必须用
.env
文件。.env.development、.env.production
这类命名是框架或加载器的约定,不能当可移植标准。含密钥的
.env 不要提交。
JSON 与 JSONC
RFC 8259 把
JSON 定义成一棵树:对象、数组,加上字符串、数字、布尔和
null。对象的键只能是字符串。没有日期、二进制、注释、引用,也不能表达循环。
{
"server": {
"host": "127.0.0.1",
"port": 8080
},
"features": ["search", "billing"]
}手写配置时,没有注释、不允许尾逗号,改起来别扭。JSONC(JSON with Comments)是允许注释的方言;VS Code 的配置文件用 JSONC,接受但不鼓励尾逗号。JSONC、JSON5 这类方言文件,不再是任意标准 JSON 解析器都能读的 JSON。JSON 更适合由工具生成和回写、人只偶尔看一眼的配置。
TOML
TOML 1.1.0
的目标是:语义明显、易读,并且无二义地映射到哈希表。名字来自作者 Tom
Preston-Werner:Tom’s Obvious, Minimal
Language。文件从根表出发,由键值、数组和嵌套表组成一棵树。标量包括字符串、整数、浮点、布尔,以及带时区日期时间、本地日期时间、本地日期、本地时间。没有
null,没有共享引用,也没有循环。规范没有规定表的最大嵌套层数。
name = "demo"
[server.database.pool]
max_connections = 10
timeout = 30[server.database.pool] 就是
server → database → pool
三层子表。Cargo.toml、pyproject.toml
是典型用例。规则少、类型写在字面量里、注释是语法的一部分。深层嵌套时表头路径会变长,长列表和深层树也不如
YAML 紧凑。
YAML
YAML 的节点只有三种结构,具体类型用标签表示。
| 序号 | 节点结构 | YAML 1.2.2 Core Schema 标签 | 含义 |
|---|---|---|---|
| 1 | 标量(scalar) | str、null、bool、int、float |
单个值 |
| 2 | 序列(sequence) | seq |
有顺序的节点列表 |
| 3 | 映射(mapping) | map |
“键 → 值”的对应关系 |
YAML 1.2.2 Core Schema 用表中这七个标签;应用还可以用显式标签或自定义 Schema 扩展类型。
YAML 可以用 anchor 和 alias 共享节点:
defaults: &config
retries: 3
timeout: 30
service_a: *config
service_b: *config&config 给节点命名,*config
指向它。三处共用同一份数据,完整 YAML
因此是图;不用引用时,可以把它当树。Kubernetes、Docker Compose 和许多 CI
流水线都用 YAML。
缩进就是语法。有些缩进错误不会报语法错,只是把数据结构改掉了。anchor 与 alias 能少写重复,读和改的成本也会上去。
XML
XML 由元素、属性、文本和命名空间构成,数据模型是一棵带属性的元素树。
<server host="127.0.0.1" port="8080">
<pool maxConnections="10" timeout="30"/>
</server>语法严谨,XSD(XML Schema
Definition)校验成熟,标记也冗余,手写成本高。它是 Java 和 .NET
生态里的历史主流:Maven 的 pom.xml、Spring 配置、.NET 的
.config 仍大量使用。没有既有生态或 Schema
约束时,新项目通常不会先选它。
代码即配置
Django 的 settings.py、Neovim 的
init.lua、Webpack 的
webpack.config.js,是直接拿代码当配置。
宿主语言里可以写条件、循环、函数,也能做类型检查,表达力远超纯数据格式。代价是配置不再是纯数据。读取它通常意味着执行或解释代码,跨语言处理和静态校验都更困难。
HCL(HashiCorp Configuration
Language)是另一路:声明式配置语言。Terraform
语言用块、参数和表达式描述目标状态,支持引用、条件、for
表达式和函数,但不是可执行任意过程的通用语言。
对照总表
| 序号 | 格式 | 数据模型 | 注释 | 类型系统 | 复用/引用 | 嵌套能力 | 典型场景 |
|---|---|---|---|---|---|---|---|
| 1 | INI | 分区 + 扁平键值 | ✓ | 无(全字符串) | ✗ | 一层分区 | Git、pytest 等简单工具 |
| 2 | .env | 扁平键值 | ✓ | 无(全字符串) | ✗ | 无 | 环境变量注入 |
| 3 | JSON | 树 | ✗(JSONC 支持) | str / num / bool / null | ✗ | 任意深度 | 工具生成与回写的配置 |
| 4 | TOML | 树 | ✓ | 标量 + 日期时间 | ✗ | 任意深度,深层冗长 | 项目元数据、应用配置 |
| 5 | YAML | 图(anchor/alias) | ✓ | 标签制,可扩展 | ✓ | 任意深度 | DevOps、CI/CD、K8s |
| 6 | XML | 带属性的元素树 | ✓ | 依赖 XSD | 实体引用 | 任意深度 | Java / .NET 存量系统 |
| 7 | 宿主语言代码 | 取决于宿主语言的值模型 | ✓ | 宿主语言类型 | ✓ | 任意 | 构建工具、应用配置 |
哈希表
映射是一组“键 → 值”。YAML 叫映射(mapping),JSON
叫对象(object),Python 叫字典(dict),Java 叫 Map。
程序读配置,就是按名字取值。键值再加上数组表示重复项,已经覆盖绝大多数需求。所以这些格式的数据模型都围着哈希表转,差在哈希表之外的规则:
| 序号 | 格式 | 和哈希表的关系 |
|---|---|---|
| 1 | .env | 一张扁平哈希表(键 → 字符串) |
| 2 | INI | 两层哈希表:外层是 section,内层是配置项 |
| 3 | TOML | 哈希表套哈希表的树,值可含子表、数组和标量 |
| 4 | JSON | 同样是树,但顶层还可以是数组或标量 |
| 5 | YAML | 映射节点是哈希表;anchor/alias 会让结构变成图 |
| 6 | XML | 不是哈希表:子节点有序,属性与子元素并存,还有文本节点 |
| 7 | 宿主语言代码 | 没有固定数据模型,是宿主语言里的任意值 |
XML 是例外。子元素是有序列表,不是按键索引;属性和子元素谁表示配置项,也没有唯一答案。要把 XML 映射成哈希表,得额外约定。TOML 官方简介的承诺是“无二义性地映射为一个哈希表”,解析结果不但是哈希表,还是哈希表套成的树。YAML 一旦用了引用,这一条就做不到。
类型从哪里来
| 序号 | 格式 | 标量类型 | null | 日期时间 | 类型如何确定 |
|---|---|---|---|---|---|
| 1 | .env | 仅字符串 | ✗ | ✗ | 无类型概念,全靠应用自己解析 |
| 2 | INI | 仅字符串 | ✗ | ✗ | 同上 |
| 3 | JSON | 字符串、数字、布尔、null | ✓ | ✗(靠 ISO 8601 字符串约定) | 显式字面量语法 |
| 4 | TOML | 字符串、整数、浮点、布尔、日期时间四种 | ✗ | ✓ 原生支持 | 显式字面量语法 |
| 5 | YAML 1.2 Core | str/null/bool/int/float + 可扩展标签 | ✓(null、~、空值三种写法) |
✗(可由自定义 Schema 扩展) | Core Schema 解析规则与显式标签 |
| 6 | XML | 全是文本节点 | 需 xsi:nil(依赖 Schema) |
需 Schema | 格式本身无类型,类型来自 XSD |
| 7 | 宿主语言代码 | 宿主语言的全部类型 | 取决于宿主语言 | 取决于宿主语言 | 编程语言本身 |
JSON 和 TOML 把类型写进字面量:"3"
是字符串,3 是数字,true
是布尔,看见就确定。YAML
对裸标量做推断,类型由解析规则判定。INI、.env、XML
没有类型,应用程序拿到文本后再转。
数字上,JSON 的语法不区分整数和浮点;TOML
区分,并且有十六进制、八进制、二进制和 inf /
nan。YAML 1.1 还支持六十进制(1:20 解析为
80),1.2 已去掉。特性越多,越要先固定版本和 Schema。
null 也不是谁都有。JSON 和 YAML 有。TOML
刻意没有:键要么存在且有值,要么不存在。INI 和 .env
用空字符串或省略键表示。键的类型也不一样:JSON、TOML、INI、.env
的键只能是字符串;YAML 的映射键可以是任意类型。
在这里比较的六种数据格式里,只有 TOML 的当前核心语法原生定义日期时间字面量,并且区分带偏移日期时间、本地日期时间、本地日期和本地时间。YAML 1.2 Core Schema 不含时间戳;XML 可以靠 XSD 拿到日期时间类型。
类型不能压成一条排名。TOML 的内建标量比 JSON 丰富,YAML 可以用标签扩展,XML 可以通过 Schema 获得完整类型系统,宿主语言配置则继承运行时。更有用的问题是:类型由文本显式决定,还是依赖解析器、Schema 或应用代码。
同一份文本,两种结果
二义性指同一份文本存在不止一种合法解析结果。常见来源有四类。
裸标量的类型猜测。解析器对不加引号的词自行判断类型,规则还随规范版本变。YAML
1.1 里 country: NO 是布尔值
false,1:20 是数字 80(六十进制);YAML 1.2
Core Schema 里它们都是字符串。解析器若仍按 1.1
规则走,同一裸词就会得到不同类型。
规范未定义的行为。JSON 规定对象键应当唯一,但没说重复键怎么处理。不同解析器可能后者覆盖、前者生效,或直接报错。
解析依赖文本之外的约定。YAML 的类型取决于选用哪个 Schema。XML 映射成键值时,属性与子元素谁表示配置项,没有标准答案。
引用与共享结构。YAML 的 anchor / alias
使解析结果成为图,甚至可以自引用成环。“映射为普通字典”没有唯一定义。常见的
<< 合并键来自 YAML 1.1 类型库,不属于 YAML 1.2 Core
Schema,支不支持取决于解析器。
TOML 把“无二义地映射为哈希表”写成设计目标,并用这些规则收紧解析自由度:
| 序号 | 二义性来源 | TOML 的对策 |
|---|---|---|
| 1 | 裸标量类型猜测 | 类型全靠显式语法:字符串必须加引号,布尔只有 true /
false,数字和日期各有明确字面量,不允许裸词 |
| 2 | 规范未定义行为 | 重复键直接报解析错误 |
| 3 | Schema 依赖 | 无类型标签、无 Schema 选择;仍需约定 TOML 规范版本 |
| 4 | 引用与共享 | 没有 anchor / alias,解析结果永远是树 |
| 5 | 空值与缺失 | 没有 null:键要么存在且有值,要么不存在 |
有两种“多个结果”不要混。TOML 里 [a.b] 表头和点号键
a.b = 1 是同一份结构的两种写法,属于“多个文本 →
同一个结果”,是语法糖。二义性是“同一个文本 → 多个结果”。TOML
只允许前者。
二义性来自解析结果依赖文本之外的东西:解析器对未定义行为的取舍,或 Schema 的选择。
注释
配置进版本控制、接受评审时,注释用来记下“为什么这么配”。RFC 8259
定义的 JSON 语法不含注释。需要注释时,只能写 _comment
这类普通字段,或明确改用 JSONC、JSON5。
| 序号 | 格式 | 注释语法 | 备注 |
|---|---|---|---|
| 1 | INI | ; 和 # 行注释 |
规范不统一,有的解析器只认 ;;行内注释因实现而异 |
| 2 | .env | # 行注释 |
行内不统一:KEY=value # 注释 里的 #
算不算注释,各库答案不同 |
| 3 | JSON | 无 | JSONC / JSON5 等方言补充注释 |
| 4 | TOML | # 行注释 |
无块注释;注释不参与键和值的解析 |
| 5 | YAML | # 行注释 |
无块注释;# 前必须有空格,否则是值的一部分 |
| 6 | XML | <!-- --> 块注释 |
不能嵌套,注释体内不允许出现 -- |
| 7 | 宿主语言代码 | 继承宿主语言 | Python #、Lua --、JS // |
数据格式普遍用行注释,因为配置按行 diff。XML
的块注释是标记语言的遗留。YAML 里 key: value#注释 的
# 是值的一部分,key: value # 注释
才是注释——和缩进一样,属于改了语义还不报错的陷阱。JSON
不收录注释,是因为它优先当数据交换格式;TOML
面向人维护的配置,注释从一开始就在语法里。
语法糖
语法糖是同一份结构的多种等价写法。它增加认知和 diff
成本,但不会让同一份文本读出两种结构。若只按等价写法的数量粗略比,顺序大概是
YAML > XML > TOML > INI / .env >
JSON。宿主语言代码不适合放进这条序列。
JSON
的值语法选择很少:字符串只能双引号,无尾逗号,无简写。空白、对象成员顺序、字符转义和数字写法仍能写出不同文本;要做稳定哈希或逐字比较,还得另用
JSON
Canonicalization Scheme 一类规则。INI 和 .env
基本无糖;仅有的写法差异(: 代替
=、export
前缀)因为格式无标准,并不能当真。
TOML
的糖是给手写用的:嵌套有表头、点号键、内联表三种等价写法;字符串有四种语法;数字支持下划线分组和多进制;TOML
1.1 的数组和内联表都允许尾逗号。YAML 最多:块式与流式两种风格(JSON 是
YAML 1.2 的子集)、多种标量风格、块标量加 chomping 指示符,再加上 anchor
/ alias。XML 的糖是结构性的:<a/> 与
<a></a> 等价,属性与子元素两种建模,CDATA
与实体引用二选一。
| 序号 | 类别 | 糖 | JSON | TOML | YAML | INI / .env | XML |
|---|---|---|---|---|---|---|---|
| 1 | 书写 | 键省略引号 | ✗ 必须双引号 | ✓ bare keys | ✓ 裸标量 | 天然无引号 | 天然(标签名) |
| 2 | 书写 | 同构嵌套多种写法 | ✗ 唯一写法 | ✓ 表头 / 点号键 / 内联表 | ✓ 块式 / 流式 | ✗ | ✓ 属性 vs 子元素 |
| 3 | 书写 | 列表项免逗号 | ✗ | ✗ | ✓ - 换行分隔 |
无列表 | 无列表 |
| 4 | 排版 | 尾逗号 | ✗(JSONC 接受) | 数组与内联表 ✓(1.1) | 流式集合 ✓,块式无此问题 | — | — |
| 5 | 排版 | 多行字符串 | ✗ 只能 \n |
✓ """、''' |
✓ |、> 加 chomping |
INI 部分解析器续行;.env ✗ | ✓ 文本节点天然多行 |
| 6 | 词法 | 布尔多拼写 | ✗ | ✗ | ✓ true/True/TRUE;1.1 的 yes/no 越界成歧义 |
— | — |
| 7 | 词法 | null 多写法 | ✗ 一种 | ✗ 无 null | ✓ null、~、空值 |
— | — |
| 8 | 词法 | 字符串多语法 | ✗ 一种 | ✓ 四种 | ✓ 三种风格 + 块标量 | — | — |
| 9 | 结构 | 引用与合并 | ✗ | ✗ | anchor / alias ✓;合并键取决于实现 | ✗ | 实体引用勉强算 |
读者得认识所有写法。语义相同的文本可以长得完全不同,diff
变吵,团队往往只能靠 formatter 把输出拧成一种。YAML 的
true/True/TRUE 还在糖这一侧,yes/no/on/off 在
1.1
里已经变成歧义。写法越少,生成器和格式化工具越好统一输出;少糖并不等于文本已经
canonical。少糖通常更利于机器处理,多糖通常更照顾手写。
怎么选
这个领域没有权威排行榜。没有标准组织在追踪配置格式的采用率;GitHub 基于 Linguist 的语言统计会排除 JSON、YAML 这类数据语言;包管理器下载量也无法公平比较内置解析器和第三方依赖。
JSON 同时覆盖数据交换和机器生成配置。YAML 常见于 DevOps
和声明式资源。TOML 常见于项目元数据和中小型应用配置。XML、INI
多见于既有生态。.env
只解决环境变量的本地加载,不表达复杂结构。
Kubernetes API 接受 JSON,日常清单通常写成 YAML。Rust 的
Cargo.toml 和 Python 的 pyproject.toml
则直接把 TOML 写进了工具约定。
| 序号 | 情况 | 选择 |
|---|---|---|
| 1 | 结构扁平的简单工具配置 | INI |
| 2 | 在本地文件里加载环境变量 | .env,前提是加载器明确支持 |
| 3 | 主要由程序读写,人只查看 | JSON |
| 4 | 需要人工注释的 JSON 类配置 | 目标工具支持时用 JSONC 或其他明确方言 |
| 5 | 项目元数据或中小型应用配置,要简单、严格、类型明确 | TOML |
| 6 | 层级深、要注释和片段复用,而且工具链已经是 YAML | YAML,并配合 JSON Schema、kubeconform 一类校验 |
| 7 | 已有 Java / .NET 技术栈和 Schema 约束 | 沿用 XML,不主动引入 |
| 8 | 确实需要任意条件、循环和生成逻辑 | 宿主语言代码,并接受执行风险与跨语言处理成本 |
| 9 | 基础设施需要引用、条件和集合变换,但仍希望保持声明式 | Terraform 语言、HCL 等目标工具规定的 DSL |
参考资料
- RFC 8259:JSON 数据交换格式
- Visual Studio Code:JSON with Comments
- TOML 1.1.0 规范
- YAML 1.2.2 规范
- YAML 1.1 Merge Key 类型
- W3C:XML 1.0(第五版)
- W3C:XML Schema 1.0
- The Twelve-Factor App:Config
- HashiCorp:Terraform 配置语言
- Cargo Reference:The Manifest Format
- Python
Packaging:
pyproject.toml规范 - Kubernetes:Objects in Kubernetes
查看本文修订记录(2)
- v1.1 · 2026.08.31 调整章节层级与结构,压缩重复对照,改写措辞。
- v1.0 · 2026.08.31 首次发布。