如何将 YAML 转换为 JSON:需要避免的常见陷阱
为什么 YAML 和 JSON 并不像看起来那样可以互换
YAML 和 JSON 看起来很相似,它们的关系也很密切。YAML 1.2 甚至是 JSON 的一个超集,所以任何有效的 JSON 也是有效的 YAML。听起来很棒,对吧?的确如此,直到你转换一个真实世界的 YAML 文件,然后发现你的第一个静默数据损坏。这两种格式的设计目标根本不同。JSON 是为机器构建的:严格、无歧义,并且没有注释的余地。而 YAML 则是为人类构建的。它使用缩进来表示结构,支持多行字符串,允许行内注释,并具有一个试图猜测你意图的类型推断系统。正是这种“乐于助人”的猜测,让转换过程出了岔子。YAML 解析器可能会读取字符串 'yes' 并将其解释为布尔值 `true`。它可能会看到 '1.0' 并生成一个浮点数,而不是你输入的字符串。这些都不是 bug,YAML 规范就是这样设计的。问题在于 JSON 没有这种模糊性。一旦你的 YAML 值在解析后的数据中变成了布尔值,JSON 输出就会写入 `true`,而原始字符串就永远丢失了。当你在为 Kubernetes 集群、OpenAPI 规范或 CI/CD 管道转换配置文件时,这些静默的类型更改可能会在不产生任何错误信息的情况下,破坏下游的所有流程。要可靠地转换文件,你必须理解这些根本性的差异。
最快的转换方法:使用 CocoConvert
当你只是需要转换一个文件时,最快的方法是使用专用工具,而不是临时拼凑一个脚本。CocoConvert 的 [YAML 转 JSON 转换器](/convert/yaml-to-json) 会处理所有的解析和序列化工作,立即为你提供格式正确、UTF-8 编码的输出。过程极其简单:粘贴你的 YAML,或上传一个 .yaml 或 .yml 文件,然后点击“转换”。你的 JSON 就会出现在输出面板中,可以直接复制或下载。CocoConvert 使用现代的 YAML 1.2 解析规则,所以你不会被旧的“挪威问题”(Norway Problem)坑到,那个问题是字符串 'NO' 被错误地解释为布尔值 `false`。如果你的源 YAML 文件有缩进错误,你会得到一个带行号的清晰解析错误,而不是被静默地破坏输出内容。它还能正确处理多文档 YAML 文件(那些带有 `---` 分隔符的文件)。这些文件会被转换成一个 JSON 数组,其中每个文档都成为一个数组元素。这是标准的、预期的行为,但如果你因为文件以 `---` 开头而在输出中看到了一个意料之外的数组,最好记住这一点。有一个限制:该工具不支持引用同一文件中不同文档节点的 YAML 锚点和别名。对于那些涉及跨文档锚点的复杂情况,你需要先手动或用本地脚本解决它们,然后再上传文件进行转换。
YAML 类型强制转换:最坑人的陷阱
类型强制转换是导致从 YAML 转换为 JSON 时数据丢失的头号原因。在转换任何生产文件之前,你绝对必须检查这些特定的陷阱。 **来自意想不到的字符串的布尔值。** 旧的 YAML 1.1 解析器(比如 6.0 版本之前的 PyYAML)会将 `yes`、`no`、`on` 和 `off` 解释为布尔值。现代的 YAML 1.2 只会这样处理 `true` 和 `false`,但如果你的源文件是由旧工具创建的,它可能包含 'yes',而它的本意确实是字符串 'yes'。如果你不知道文件的来源,就需要手动检查这些值。 **八进制整数。** 这个是经典问题了。在 YAML 中,像 `0755` 这样的值会被解析为八进制整数 493。这在 Kubernetes 清单中用于设置文件权限时是个臭名昭著的陷阱。转换后,你的 JSON 将包含数字 `493`,而不是字符串 `'0755'`。如果下游进程试图在 `chmod` 调用中使用这个数字,权限会完全错误,而且你还不会收到任何报错。 **浮点数的边界情况。** YAML 能理解像 `.inf`、`-.inf` 和 `.nan` 这样的特殊浮点值,但 JSON 不能。CocoConvert 的处理方式是将它们转换为字符串 'Infinity'、'-Infinity' 和 'NaN'。这是一个明智的降级处理,但如果你的应用程序只期望数字,它可能会在处理这些字符串值时失败,需要进行后处理。 **Null 的表示方式。** YAML 对 null 的处理很灵活,接受 `null`、`~`,甚至只是一个键后面的空值。所有这些在 JSON 中都会变成标准的 `null`。这通常没问题,但请记住,冒号后面没有任何值的键会变成 JSON 的 `null`,而不是空字符串 `""`。
处理多行字符串和注释
YAML 提供了两种强大的多行字符串语法,在 JSON 中没有直接的对应物:字面量块标量(`|`)和折叠块标量(`>`)。字面量块(`|`)会保留每一个换行符。折叠块(`>`)会将单个换行符转换为空格,但保留双换行符作为实际的换行。这两种语法都会生成一个单一的 JSON 字符串,但换行符处理上的细微差别对于像 shell 脚本、SQL 查询或证书这样的嵌入式内容至关重要。 例如,这个 YAML: ```yaml script: | echo hello echo world ``` 会变成这个 JSON: ```json {"script": "echo hello\necho world\n"} ``` 注意,使用字面量 `|` 风格时,末尾的换行符(`\n`)默认被保留了。要去除它,你需要使用 chomping 指示符 `|-`。任何调试过因细微空白差异而失败的 CI 脚本的人都懂这种痛苦。搞错这一点可能会破坏对空白敏感的脚本或 API。 注释是个更棘手的问题。YAML 使用 `#` 支持注释。JSON 不支持。就这样。这意味着在转换过程中,你 YAML 文件中的每一条注释都会被永久删除。所有解释*为什么*要设置某个值的关键上下文——在基础设施即代码中很常见的做法——都会从 JSON 输出中消失。在 JSON 规范内没有解决办法。我的建议很简单:始终将带注释的 YAML 文件作为事实的唯一来源,而将生成的 JSON 文件视为一次性的构建产物。有些团队使用 JSONC(带注释的 JSON),但这只是把兼容性问题往后拖延而已。
锚点、别名和合并键
YAML 的锚点和别名是保持文件 DRY(Don't Repeat Yourself,不要重复自己)的绝佳特性,但它们给 JSON 转换带来了复杂性。你用 `&anchor-name` 定义一个锚点,然后用 `*anchor-name` 引用它。YAML 解析器在读取文件时会展开这些别名,在内存中构建最终的数据结构。因此,JSON 输出会包含完全展开、重复的内容,没有任何原始锚点的痕迹。 考虑这个常见的模式: ```yaml defaults: &defaults timeout: 30 retries: 3 production: <<: *defaults host: prod.example.com staging: <<: *defaults host: staging.example.com ``` `<<` 语法是 YAML 的合并键。最终的 JSON 将是: ```json { "defaults": {"timeout": 30, "retries": 3}, "production": {"timeout": 30, "retries": 3, "host": "prod.example.com"}, "staging": {"timeout": 30, "retries": 3, "host": "staging.example.com"} } ``` 展开是正确的,但原始 YAML 的简洁性消失了。如果有 50 个服务继承了那个默认锚点,那么 JSON 文件将包含该数据的 50 份副本。对于机器来说,这完全没问题。但对于试图阅读文件的人,或者对于关注文件大小的系统来说,这是一个显著的缺点。 请注意,合并键支持(`<<`)在技术上是 YAML 的一个扩展,不属于核心规范,所以一些严格的解析器会拒绝它。CocoConvert 处理合并键没有问题。如果你用 Python 的 PyYAML 编写转换脚本,必须使用 `yaml.full_load()` 或 `yaml.safe_load()`。避免使用不带 `Loader` 参数的旧 `yaml.load()`,由于存在重大安全风险,它自 PyYAML 5.1 起已被弃用。
用代码转换 YAML 到 JSON
对于批量转换、构建管道集成或任何类型的自动化处理,你需要一个命令行或脚本化的解决方案。Web 工具非常适合一次性转换,但自动化需要代码。以下是几种最可靠的方法。 **Python(最便携的选项):** ```python import yaml, json, sys with open(sys.argv[1], 'r') as f: data = yaml.safe_load(f) print(json.dumps(data, indent=2, ensure_ascii=False)) ``` 务必使用 `yaml.safe_load()`。旧的 `yaml.load()` 是一个安全噩梦,可以从恶意的 YAML 文件中执行任意代码。`ensure_ascii=False` 参数也是一个好习惯,因为它会保留 Unicode 字符,而不是对它们进行转义。 **Node.js:** ```javascript const yaml = require('js-yaml'); const fs = require('fs'); const data = yaml.load(fs.readFileSync(process.argv[2], 'utf8')); console.log(JSON.stringify(data, null, 2)); ``` `js-yaml` 库默认使用现代的 YAML 1.2 规则(自 v4.0 起)。如果你在一个旧项目工作,请仔细检查你的 `package.json`。4.0 之前的版本使用 YAML 1.1 规则,会错误地将 'yes' 和 'no' 等字符串强制转换为布尔值。 **yq(命令行工具):** ```bash yq -o=json eval '.' input.yaml > output.json ``` 坦白说,`yq` 是在命令行上完成这项工作的最佳工具。它是一个专门构建的 YAML 处理器,能正确处理所有情况——多文档文件、锚点、合并键——并且只需一个简单的标志即可输出 JSON。在 macOS 上用 Homebrew 安装(`brew install yq`),或者在 Linux/Windows 上从 GitHub 获取二进制文件。 当然,如果想不安装任何东西快速转换,[CocoConvert 的 YAML 转 JSON 工具](/convert/yaml-to-json) 仍然是完成任务的最快方式。
在使用前验证你的输出
转换文件而不进行验证,是给生产环境引入潜在 bug 的绝佳配方。一个 JSON 文件可能在语法上完全有效,但包含语义上错误的数据,比如我们讨论过的类型强制转换。这里有一个实用的清单,可以让你免去未来的头痛。 **语法验证。** 至少,将输出通过一个 JSON linter 运行一遍。你的代码编辑器(如 VS Code 或 JetBrains IDE)可能已经自动做了这件事。在命令行上,Python 内置的 `json.tool` 是一个可靠的主力工具:`python3 -m json.tool output.json > /dev/null`。对于有效的 JSON,它会以代码 0 退出,如果失败,它会准确地告诉你哪里出了问题。 **模式验证。** 对于关键文件,请使用模式(schema)。如果你的目标格式有 JSON Schema(这在 OpenAPI 规范、AWS CloudFormation 和 Kubernetes CRD 中很常见),就用它来进行验证。像 `ajv-cli` 这样的工具(`ajv validate -s schema.json -d output.json`)会捕捉到简单语法检查无法发现的类型不匹配问题。 **与已知良好版本进行差异比较。** 当你有一个参考的 JSON 文件时,进行差异比较至关重要。但首先,要将键的顺序规范化,以避免产生嘈杂、无意义的差异。`jq` 工具可以确定性地对键进行排序:`jq --sort-keys . output.json > normalized.json`。记住,JSON 中的键顺序不重要,但当你想比较文件时,它会把你逼疯。 **抽查被强制转换的类型。** 如果你怀疑你的 YAML 中有像 '1.0' 或 '0755' 这样的值,直接检查 JSON 输出。快速执行 `grep -n "0755" output.json` 会立即告诉你,你的八进制字符串是幸存下来了,还是被转换成了一个无用的整数。 说真的,花五分钟验证你的输出,永远比调试一个由本应是字符串的布尔值引起的生产事故要快。