JSON 工具箱

JSON 与 YAML、XML 的互转边界

结论:JSON 是 YAML 的子集,这句话只保证了单向成立;而 XML 根本没有官方 JSON 映射,全靠约定。

进阶约 12 分钟发布于 2026-10-30更新于 2026-10-30
JSON 与 YAML、XML 的互转边界封面

结论:以 JSON 作为中间格式最稳妥:先把源格式解析成数据模型,再序列化为目标格式;转换前先确认注释、XML 属性与命名空间、以及数字和布尔类型是否需要保留。

一条转换链路能不能用,不取决于它转得多漂亮,而取决于它丢了多少东西你有没有意识到。

一、JSON 与 YAML:单向包含

YAML 1.2 规范明确写了:JSON 是 YAML 的一个子集。也就是说任何合法 JSON 都是合法 YAML:

{"name": "json", "tags": ["a", "b"], "count": 3}

这段既是 JSON,也是 YAML 的流式写法。反过来不成立。

特性 JSON YAML 1.2
注释 ✗ ✓
锚点与别名 ✗ ✓
多文档(---) ✗ ✓
非字符串键 ✗ ✓(1: a、true: b)
复杂键 ✗ ✓(? [a, b])
缩进敏感 ✗ ✓(不接受 Tab 缩进)
类型推断 显式 隐式(0o17、0x1F、.inf)
单行可读性 好 差(长内容靠折叠语法)

1.1 类型推断是最大的坑

YAML 会把一些看起来像字符串的值解析成别的类型:

zip: 01234        # 字符串还是八进制整数?YAML 1.2 core schema 视为字符串
version: 1.10     # 1.1(浮点),语义丢失
country: NO       # YAML 1.2 是字符串;YAML 1.1 是布尔 false
date: 2026-10-30  # 部分实现会解析成日期对象

NO 这个例子害过很多人:使用 YAML 1.1 解析器(部分老库)时,country: NO 变成 false。 对策:所有「长得像数字或布尔」的标识符一律加引号,version: "1.10"。

1.2 锚点、别名与多文档

defaults: &defaults
  retries: 3
  timeout: 30

serviceA:
  <<: *defaults
  host: a.example.com

---
serviceB:
  <<: *defaults
  host: b.example.com

展开锚点后转成 JSON,通常会得到两份重复的配置对象,或者一个数组(多文档必须转成 [{...}, {...}])。 无论哪种,反向转换时锚点不会恢复——这是必然的有损。

1.3 保留注释的转换

多数库转换时直接丢注释。需要保留时用 yaml 包的 Document API:

import { parseDocument } from 'yaml';

const document = parseDocument(`
# 服务配置
port: 8080
`);

document.set('port', 3000);
console.log(document.toString());
// # 服务配置
// port: 3000

判断标准很简单:如果这份文件会被人反复编辑,就别用「解析 → 对象 → 序列化」的路径。

1.4 往返一致性怎么验

/**
 * 校验 JSON → YAML → JSON 的往返一致性。
 * @param sourceValue 原始 JSON 值
 * @param stringify YAML 序列化函数
 * @param parse YAML 解析函数
 * @returns 是否完全一致
 */
function isRoundTripStable(
  sourceValue: unknown,
  stringify: (input: unknown) => string,
  parse: (input: string) => unknown,
): boolean {
  const yamlText = stringify(sourceValue);
  const restored = parse(yamlText);
  return JSON.stringify(restored) === JSON.stringify(sourceValue);
}

用真实数据跑一遍,比读十遍文档有用。重点构造这些样例:null、空字符串、空数组、"00"、"1e5"、 负零、超过 2^53 的整数、含换行的长字符串。

二、JSON 与 XML:没有标准的映射

XML 比 JSON 早得多,结构能力也更强(属性、命名空间、混合内容、DTD)。两者之间没有官方映射规范, 所有工具都在做同一件事:选一套约定,然后尽量别丢。

2.1 常见约定

约定 表示 示例
元素 → 对象键 同名元素变字段 <a>1</a> → {"a": "1"}
属性 → @ 前缀 @name <a id="1"/> → {"a": {"@id": "1"}}
文本 → #text 有属性又有文本时 <a id="1">hi</a> → {"a": {"@id": "1", "#text": "hi"}}
重复元素 → 数组 出现两次以上则是数组 <i>1</i><i>2</i> → {"i": ["1","2"]}
命名空间 → 前缀 ns:tag <ns:tag/> → {"ns:tag": ...}
<order id="A1001">
  <item sku="X1" qty="2">笔记本</item>
  <item sku="X2" qty="1">钢笔</item>
</order>
{
  "order": {
    "@id": "A1001",
    "item": [
      { "@sku": "X1", "@qty": "2", "#text": "笔记本" },
      { "@sku": "X2", "@qty": "1", "#text": "钢笔" }
    ]
  }
}

2.2 必然有损的五处

  1. 元素顺序:JSON 对象无序,XML 是有序的元素序列;
  2. 属性与子元素:在 XML 里语义不同,在 JSON 里都变成字段;
  3. 混合内容:<p>你好 <b>世界</b> 再见</p> 这类结构无法用普通对象表达;
  4. 命名空间声明:xmlns 是作用域概念,转成扁平字段后作用域丢失;
  5. CDATA 与实体:<![CDATA[...]]> 与 &amp; 在 JSON 里都只是文本。

2.3 「一个元素还是数组」的两难

<list><item>1</item></list>

只有一个 item 时,xml2js 默认给对象而不是数组;出现两个才给数组。 这导致前端代码必须写 Array.isArray(x) ? x : [x]。

对策:解析时统一开启「始终数组」选项,或依据 schema 声明字段为数组类型。

import { parseStringPromise } from 'xml2js';

const parsed = await parseStringPromise(xmlText, {
  explicitArray: true,   // 单元素也返回数组
  trim: true,
  explicitRoot: false,
});

2.4 安全提醒

解析不可信 XML 前必须禁用外部实体,否则可能触发 XXE(读取本地文件、内网探测):

// Node 生态通常通过解析器选项关闭 DTD 与外部实体
const parserOptions = { doctype: false, externalEntities: false };

XML → JSON 只是转换的第一层,转换并不消除 XML 带来的攻击面。

三、三种格式的适用位置

场景 推荐格式 原因
对外 HTTP 接口 JSON 浏览器与所有语言原生支持
人工维护的配置 YAML 注释、缩进、可读性
需要强 schema 与命名空间的文档(SOAP、Office Open XML、SVG) XML 结构能力与既有生态
与运维工具交互(K8s、CI) YAML 生态默认
数据落盘、缓存、日志 JSON / JSONL 通用、零依赖

四、转换实现要点

import { stringify as toYaml } from 'yaml';

/**
 * 把 JSON 值转成 YAML 文本,保留中文不转义。
 * @param value 待转换的 JSON 值
 * @returns YAML 文本
 */
function jsonToYaml(value: unknown): string {
  return toYaml(value, {
    indent: 2,
    lineWidth: 0,      // 不自动折行,避免长字符串被切
    defaultStringType: 'QUOTE_DOUBLE',
  });
}

几个实用参数:

  • lineWidth: 0 关闭自动折行,否则长 URL 会被切成多行,人眼难读;
  • 统一双引号,可避免 NO、1.10 这类值被误判;
  • 数字精度敏感的数据,先转成字符串再序列化。

XML 方向建议固定一份映射函数,把「属性前缀」「文本键名」「数组策略」写进函数签名,而不是散落在各处:

type XmlMappingOptions = {
  attributePrefix: string;
  textKey: string;
  alwaysArray: boolean;
};

const defaultMapping: XmlMappingOptions = {
  attributePrefix: '@',
  textKey: '#text',
  alwaysArray: false,
};

五、检查清单

  • [ ] YAML 里所有「像数字/布尔」的标识符加了引号;
  • [ ] 明确 JSON → YAML → JSON 的验证样例集,并纳入测试;
  • [ ] 需要保留注释时走 Document API,不做对象往返;
  • [ ] XML 映射约定写进文档,团队共用一套参数;
  • [ ] 解析不可信 XML 时禁用 DTD 与外部实体;
  • [ ] 转换前后的字节数、字段数做一次对照,异常即报警。

常见问题

JSON、YAML、XML 之间应该如何选择转换方向?

优先把 JSON 作为中间格式:YAML 与 XML 都先解析成数据模型,再从模型输出目标格式,避免直接做文本替换。需要保留注释时走 YAML Document API;需要保留 XML 属性与命名空间时,先固定一套映射约定。

为什么 XML 转 JSON 会丢失属性和命名空间?

JSON 没有属性与命名空间的概念,XML 的属性、前缀和命名空间必须被压进普通键值,常见做法是加 @ 前缀或 #text 节点。映射规则一旦不统一,同一份 XML 就会得到不同结构的 JSON,往返后无法还原。

是否可以把三种格式长期双向同步?

不建议。注释、锚点、XML 属性和命名空间在转换中不可逆,双向同步会不断累积差异。更稳妥的做法是选定一种源格式,把其他格式当作一次性导出或临时查看结果。

参考资料

相关文章

JSON 与 CSV、表格数据的互转

CSV 是二维的,JSON 是树形的。转换的核心不是写代码,而是先决定「数组怎么摊、类型怎么留」。

入门约 10 分钟更新于 2026-10-30

JSON 转 YAML 命令行:yq 一条命令背后的四个坑

教程里出现频率最高的 yq -P input.json > output.yaml,实测输出的仍然是 JSON。本文用 yq v4.44.6、PyYAML 6.0.3 与 js-yaml 在 2.39 MB 样本上跑出四组数据,讲清输出格式由扩展名决定这件事,并给出批量转换、引号、缩进与大整数精度的正确写法。

进阶约 9 分钟更新于 2026-11-03