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