Essay 011 · 工具

配置文件格式对比

比较 INI、.env、JSON、TOML、YAML 和 XML 的数据模型、类型、注释与二义性,并按工具链给出选型边界。

本次修订:调整章节层级与结构,压缩重复对照,改写措辞

Version 1.1

一句话结论:选型先看目标工具链接受什么。格式之间真正不同的,是数据模型、类型写在字面量里还是靠解析器猜、以及同一份文本会不会读出两种结果。没有既有约束时,扁平用 INI 或 .env,机器读写用 JSON,项目元数据和中小型配置用 TOML,深度嵌套且需要复用再用 YAML。

比较什么

范围限于人维护、通常进版本控制、应用在启动或重载时解析的静态配置文件。运行时动态配置、配置中心和密钥管理不在范围内。敏感值也不会因为换了一种文件格式,就可以进仓库。

这类文件会被反复编辑、diff、评审。对照时看这几项:

  1. 人能不能顺手改,注释和 diff 是否干净
  2. 能不能表达嵌套、列表和键值
  3. 字符串、数字、布尔、日期是写在字面量里,还是靠解析器猜
  4. 能不能引用一段共享配置
  5. 解析库、编辑器、校验工具是否成熟

常见格式

INI

分区(section)下的扁平键值对,值一律当字符串。

[server]
host = 127.0.0.1
port = 8080

[database]
dsn = postgres://localhost/demo

INI 没有统一的正式标准。注释符号、重复键、转义,不同解析器各写各的。它能撑住结构扁平的工具配置,嵌套一深就不够用。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.tomlpyproject.toml 是典型用例。规则少、类型写在字面量里、注释是语法的一部分。深层嵌套时表头路径会变长,长列表和深层树也不如 YAML 紧凑。

YAML

YAML 的节点只有三种结构,具体类型用标签表示。

序号 节点结构 YAML 1.2.2 Core Schema 标签 含义
1 标量(scalar) strnullboolintfloat 单个值
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 是布尔值 false1: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

参考资料

查看本文修订记录(2)
  1. v1.1 · 2026.08.31 调整章节层级与结构,压缩重复对照,改写措辞。
  2. v1.0 · 2026.08.31 首次发布。