
內容摘要
YAML 會用隱式型別表解析每一個未加引號的純量,所以 NO、yes、010 和 1:30 都不是字串。不同解析器實作不同版本的 YAML,同一份檔案在一種語言裡讀成 false,在另一種語言裡卻是 "NO"。
YAML 看起來像一串純文字,很容易讓人以為解析器會原封不動把你寫的內容交回來。它不會。YAML 會透過一張解析表幫每一個未加引號的純量指定型別,而套用哪一張表,取決於解析器實作的是哪一個 YAML 版本。
在 2026-10-09 實測,下面這三行文件會產生兩種不同結果:
countries: [GB, IE, FR, DE, NO]
flag: yes
code: 010
PyYAML 6.0.2 {'countries': ['GB', 'IE', 'FR', 'DE', False], 'flag': True, 'code': 8}
js-yaml 5.4.2 {'countries': ['GB', 'IE', 'FR', 'DE', 'NO'], 'flag': 'yes', 'code': 10}
yaml 2.9.1 {'countries': ['GB', 'IE', 'FR', 'DE', 'NO'], 'flag': 'yes', 'code': 10}
兩種結果都不是檔案的錯。PyYAML 實作的是 YAML 1.1 規則,NO 是布爾,開頭的 0 代表八進位;js-yaml 與 yaml 套件遵循 YAML 1.2 core schema,兩者都不成立。接下來拆解這個落差背後的六個陷阱,以及怎麼在它們進到正式環境之前攔下來。
01為什麼 YAML 會悄悄改掉你的資料
YAML 文件裡的每個節點都會有一個 tag,沒有明確標註 tag 的純量,會依序比對一串正則表達式來決定型別。YAML 1.2.2 規範把這串規則寫在 core schema 裡:null、布爾、整數、浮點數、字串,依明確程度排序。
core schema 刻意收得很窄,只認 true、True、TRUE、false、False、FALSE 這幾種布爾寫法。YAML 1.1 寬鬆得多,它的語言無關型別庫把 y、Y、yes、Yes、YES、n、N、no、No、NO、true、True、TRUE、false、False、FALSE、on、On、ON、off、Off、OFF 全部列為布爾寫法。
這段歷史就是同一份設定檔在不同語言裡行為不同的原因。解析器不只要猜你的意圖,還得依照作者多年前選定的規範版本來猜。遇到 YAML 問題時,第一個該問的不是「檔案寫了什麼」,而是「這個解析器解出了什麼」。
驗證通過的 YAML 文件,不等於正確的設定檔。
驗證只回答文件能不能解析,完全不保證某個未加引號的純量解析成你想要的型別,而下面每一個陷阱都來自這裡。
02陷阱 1:Norway problem
Norway problem 是最經典的例子,而且真有故事。StrictYAML 項目記錄了一份國家清單:GB、IE、FR、DE 都正常,唯獨挪威出事——兩碼國碼 NO 同時也是 YAML 1.1 的 false。
countries:
- GB
- IE
- FR
- DE
- NO
交給 YAML 1.1 解析器,最後一項是布爾值 False,不是字串 "NO"。用國碼查表的程式於是找不到本來該存在的鍵,而且日誌裡沒有任何一行指向這個檔案。
修正方式是把必須維持文字的純量加上引號,或直接寫明型別:
countries:
- GB
- IE
- FR
- DE
- "NO"
strict_code: !!str NO
國碼只是最顯眼的案例。同樣的陷阱還包括州名縮寫、本來想當字串讀的 yes/no 開關,以及任何會撞上布爾寫法的短識別碼。
03陷阱 2:用 Tab 縮排
YAML 對空白極度敏感,Tab 是第一個經典錯誤。YAML 1.2.2 規範明確要求縮排不得使用 Tab 字元,所以這不是某一套函數庫的怪癖。
各家的錯誤訊息不同,這也是搜尋錯誤原文有用的原因:
PyYAML 6.0.2 ScannerError: while scanning for the next token
found character '\t' that cannot start any token (line 2, column 1)
js-yaml 5.4.2 YAMLException: tab characters must not be used in indentation (2:1)
yaml 2.9.1 YAMLParseError: Tabs are not allowed as indentation at line 2, column 1
出現在引號字串裡的 Tab 是資料,仍然合法。規範管的是縮排。請把編輯器設定成在 .yml 與 .yaml 檔案裡插入空格;如果整段是從聊天視窗或日誌貼過來的,就交給格式化工具重新縮排。
04陷阱 3:重複鍵與合併鍵
YAML 的 mapping 定義為一組鍵值對,且每個鍵都必須唯一,重複鍵在規範裡被列為載入失敗。只是各家實作吵得大小聲不同:
| 解析器 | 先 a: 1 再 a: 2 |
|---|---|
| PyYAML 6.0.2 | 載入成 {'a': 2},靜默保留最後一個值 |
| js-yaml 5.4.2 | YAMLException: duplicated mapping key (2:1) |
| yaml 2.9.1 | YAMLParseError: Map keys must be unique at line 2, column 1 |
合併鍵是同一件事、但爆炸半徑更大。<< 來自 YAML 1.1 的 merge 型別,各家處理方式不一致:PyYAML 會把錨點指向的 map 合併進子節點,js-yaml 5.4.2 與 yaml 2.9.1 則留下一字不差的 << 鍵,值就是父層那份 map。子設定檔因此可能帶著一個誰都沒預期的鍵,而不是繼承到值;重複鍵則可能悄悄蓋掉審查者已經核准的那一個。
安全相關的預設值不要靠合併鍵。
如果父層 map 放的是安全預設值,子層預期要繼承,那麼解析器留下未展開的
<<時,應用程式既拿不到預設值,也不會收到錯誤。請在 CI 裡鎖定解析器版本,並斷言解出來的物件,而不是只確認檔案能解析。
05陷阱 4:布爾與數字取決於規範版本
布爾最常被討論,但數字規則也跟著改過,而且改的方向還不一致:
| 輸入 | YAML 1.1 | YAML 1.2 core | PyYAML 6.0.2 | js-yaml 5.4.2 |
|---|---|---|---|---|
yes | 布爾 true | 字串 | True | "yes" |
on | 布爾 true | 字串 | True | "on" |
y | 布爾 true | 字串 | "y" | "y" |
010 | 整數 8,八進位 | 整數 10 | 8 | 10 |
0o10 | 字串 | 整數 8 | "0o10" | 8 |
1:30 | 整數 90,六十進位 | 字串 | 90 | "1:30" |
1e3 | 字串 | 浮點數 1000 | "1e3" | 1000 |
有兩個細節值得再看一次。YAML 1.1 的整數型別用開頭 0 表示八進位、用冒號表示六十進位,所以 1:30 是九十,不是時間。YAML 1.1 的浮點數樣式要求指數必須帶正負號,所以 1e3 會留成字串,而 1.0E+3 會變成浮點數——本機以 python3 -c "import yaml; print(yaml.safe_load('a: 1.0E+3'))" 實測,輸出 {'a': 1000.0}。
就算同樣掛著「YAML 1.1」,實作之間也會打架:型別庫把 y 列為布爾,但 PyYAML 6.0.2 留成字串。版本名稱只是起點,不是保證。
06陷阱 5:版本號、郵遞區號與長 ID
有三種日常欄位會在程式碼審查時看起來完全正常,卻在解析後壞掉:
version: 1.10會載入成浮點數1.1。數字裡的尾端零沒有意義,來回轉換就消失,版本比較因此走錯方向。zip: 01234在 PyYAML 載入成整數668,在 js-yaml 載入成1234。開頭 0 在 YAML 1.1 是八進位、在 1.2 core schema 是十進位,而兩者都不是你要的字串。- 像
12345678901234567890這種 20 位數 ID,在 JavaScript 解析器裡會變成12345678901234567000,因為 JS number 無法精確容納每一個 64 位元整數。PyYAML 會以 Python 整數完整保留,於是同一把 API key 在一個服務能用、在另一個服務失敗。
日期也是版本分界。PyYAML 6.0.2 會把未加引號的 2026-10-04 解析成 datetime.date 物件,而在本文實測的設定裡,js-yaml 5.4.2 留成字串 "2026-10-04"。如果程式期待字串卻拿到日期物件,再序列化回 JSON 就會是不同形狀。
能一次擋掉這三種狀況的規則很簡單:識別碼、版本號、代碼,以及任何你想當文字比較的值,一律加引號。欄位真的必須是數字時,就用 schema 講清楚,不要靠解析器猜。
07陷阱 6:錨點與別名
錨點與別名是 YAML 內置的重用機制:&name 標記節點,*name 插入指向它的引用,一份文件可以用很少的文字表達很大的結構。風險也在這裡。所謂 billion laughs 會把別名層層嵌套,讓一份小檔案展開成巨大的物件;沒有設下別名展開上限的解析器,可能被幾百 bytes 的文件拖垮。
在物件可變的語言裡還有更細的問題:別名是同一個節點的引用,不是複本,所以改動 *name 背後的物件,會連帶改掉錨點出現的每一個地方。別名也只能往回指:它引用的是前面已定義的錨點,所以即使結構看起來很合理,向前引用仍會失敗。
用在設定檔時,重用層次請保持淺薄,並且在 diff 或審查之前先把錨點展開。如果兩個環境本來就該不一樣,展開後的文件 diff 才是審查者真正能信任的版本。
08上線前怎麼驗證 YAML
語法檢查能攔下前面幾節的 Tab 錯誤與重複鍵錯誤。UrsaKit 的 YAML 驗證工具 在瀏覽器裡完成解析,並回報第一個無法繼續的位置。
- 把文件貼進 YAML input 欄位。
- 按下 Validate YAML。
- 看結果。合法文件會顯示綠色的 This YAML is valid.;壞掉的文件會顯示 This YAML is invalid,後面接著行號、欄位與解析器訊息。
- 修掉第一個回報的位置後再驗證一次。YAML 的錯誤會連鎖,第 40 行常常只是第 12 行的後果。
真正能抓到隱式型別的是另一個步驟:country: NO 會通過驗證,因為它是合法文件。想知道解析器解出了什麼,把同一段文字丟進 YAML 轉 JSON,直接看輸出。
{
"country": false
}
看到這一步就該補上引號。文件確認無誤後,用 YAML 格式化工具 統一縮排;它會依你選的寬度重新縮排,同時保留註釋與錨點,修正的過程不會犧牲審查脈絡。
| 症狀 | 原因 | 修正 |
|---|---|---|
NO 或 yes 變成布爾 | YAML 1.1 的隱式布爾解析 | 加引號,或標上 !!str |
對看不見的字元拋出 ScannerError | 用 Tab 縮排 | 改用空格,並用格式化工具重排 |
| 其中一個鍵默默勝出 | 重複鍵遇上寬鬆的解析器 | CI 改用 js-yaml 或 yaml 解析 |
| 子設定檔沒繼承到值 | 解析器不支援合併鍵 | 展開父層 map,或鎖定解析器 |
1.10 變成 1.1 | 版本字串被當成浮點數 | 版本號與 build number 一律加引號 |
09YAML、JSON、TOML 怎麼選
YAML 1.2 的 core schema 改動讓 YAML 成為 JSON 的嚴格超集,也移除了許多有問題的隱式型別建議,但舊規則仍活在早於 1.2、或直接忽略 1.2 的解析器裡。該選哪種格式,取決於檔案由誰撰寫、值有多重要:
| YAML | JSON | TOML | |
|---|---|---|---|
| 註釋 | 有 | 沒有 | 有 |
| 隱式型別 | 有,且隨版本改變 | 沒有 | 沒有 |
| 多行字串 | 區塊純量 | 只能轉義 | 多行基本字串 |
| 重用 | 錨點與別名 | 沒有 | 沒有 |
| 相容 JSON | 1.2 是超集 | 原生 | 不是 |
| 最適合 | 人類編輯的設定檔 | API payload 與機器輸出 | 想嚴格控制型別的人類設定檔 |
如果某個值必須原樣來回轉換,就放 JSON,或在 YAML 裡加引號。如果檔案由人類撰寫、只給一個應用程式讀,YAML 的彈性值得你用「看起來像代碼就加引號」的紀律去換。
設定檔最後要送進 API 時,轉換後先用 JSON 驗證工具 檢查 payload;要反過來把機器產生的 JSON 轉回人類可編輯的檔案,用 JSON 轉 YAML。
10資料來源
- YAML 1.2.2 Specification — Core Schema 與 Mapping Keys(取用日期:2026-10-09)
- YAML 1.1 Language-Independent Type: Boolean(取用日期:2026-10-09)
- YAML 1.1 Language-Independent Type: Integer(取用日期:2026-10-09)
- YAML 1.1 Language-Independent Type: Float(取用日期:2026-10-09)
- The Norway Problem — why StrictYAML refuses to do implicit typing(取用日期:2026-10-09)
貼上文件,直接指出第一個問題的行號與欄位,解析全程在瀏覽器裡完成。
YAML 驗證工具常見問題
YAML 裡的 Norway problem 是什麼?
就是國碼 NO 被解析成布爾值 false,因為 YAML 1.1 會把未加引號的 no 與 NO 解析成布爾。這個名字來自一份國家清單,挪威在裡面悄悄變成了 false。
為什麼 YAML 說 yes 不是布爾值?
看版本。YAML 1.1 把 yes、no、on、off 都當布爾,而 YAML 1.2 core schema 只接受 true、True、TRUE、false、False、FALSE。YAML 1.2 解析器會把 yes 留成字串。
為什麼我的 YAML 出現 Tab 錯誤?
規範禁止用 Tab 字元縮排,所以只要 Tab 出現在該放縮排的位置,解析器就會停下來。請把編輯器設定成在 YAML 檔案裡插入空格。
YAML 允許重複的鍵嗎?
不允許。同一個 mapping 的鍵必須唯一,重複鍵是規範列出的載入失敗之一。PyYAML 會靜默保留最後一個值,js-yaml 與 yaml 套件則直接報錯,同一份檔案在不同解析器下行為不同。
為什麼 1.10 變成 1.1?
未加引號的 1.10 會被讀成浮點數,而數字裡的尾端零沒有意義,所以再輸出時變成 1.1。版本字串、build number 與識別碼都要加引號,讓它們維持文字。
為什麼我的長 ID 掉了後面幾位?
JavaScript 無法精確表示每一個 64 位元整數。把 20 位數的 ID 解析成 JS number 會被四捨五入,12345678901234567890 會變成 12345678901234567000。ID 請一律用引號包成字串。
驗證通過的 YAML 一定正確嗎?
不一定。驗證器只回答文件能不能解析,不回答解析出來的值是不是你要的。country: NO 是合法的 YAML,卻仍然會載入成 false。
相關文章
- JSON 格式化、驗證與壓縮
JSON 只有一張型別表,沒有隱式轉型,值必須原樣存活時它更安全。
- 什麼是 SKILL.md?
每一份 SKILL.md 開頭都是 YAML frontmatter,正是這些型別陷阱最常出現的地方。
相關工具
- YAML 驗證工具
語法檢查,並指出第一個錯誤的行號與欄位。
- YAML 格式化工具
重新縮排,同時保留註釋與錨點。
- YAML 轉 JSON
看清解析器實際解出的值,包含型別。
- JSON 驗證工具
轉出來的 payload 進 API 之前先檢查一次。
- JSON 轉 YAML
把機器產生的 JSON 轉回可讀的設定檔。