最近在搬砖过程中,遇到一个 Go 代码里 YAML 转 JSON 引发报错的小问题,顺手记录一下。场景是这样的:我实现了一个功能,支持用户上传 YAML 或 JSON 格式的文档,为了方便统一处理,我会先把所有文档转成 JSON 格式。说实话,平时操作 YAML 和 JSON 都很频繁,但二者互转的场景确实不怎么遇到,所以一时疏忽就踩了个坑。这个 Go YAML 转 JSON 的常见问题,希望能帮到遇到类似情况的开发者。
使用 yaml.v3 包处理 YAML
先看一段 YAML 文档示例:
object: a: 1 1: 2 "1": 3 key: value array: - null_value: - boolean: true - integer: 1
这是一个对象结构,里面还嵌套了一个数组。细心的读者可能已经注意到,对象中有两个 key 长得比较像:一个是数字类型的 1,另一个是字符串类型的 "1"。这个细节在 Go YAML 解析中非常关键,容易引发键冲突问题。
在 Go 语言里,我们通常用 yaml.v3 处理 YAML,用内置的 encoding/json 处理 JSON。下面这段代码试图把上面的 YAML 文档转成 JSON:
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/icza/dyno" // 用于递归转换 map[interface{}] => map[string]
"gopkg.in/yaml.v3" // YAML 解析库
)
func main() {
yamlExample := `
object:
a: 1
1: 2
"1": 3
key: value
array:
- null_value:
- boolean: true
- integer: 1
`
// 解析 YAML
var data interface{}
if err := yaml.Unmarshal([]byte(yamlExample), &data); err != nil {
log.Fatalf("YAML parse error: %v", err)
}
fmt.Printf("Type: %TnValue: %#vn", data, data)
fmt.Println("--------------------------")
// 关键:递归转换 map 键类型(interface{} → string)
convertedData := dyno.ConvertMapI2MapS(data)
// 转换为 JSON
jsonData, err := json.Marshal(convertedData)
if err != nil {
log.Fatalf("JSON convert error: %v", err)
}
fmt.Printf("Type: %TnValue: %sn", jsonData, jsonData)
}
运行一下,结果直接报错:
$ go run main.go
2025/07/31 23:22:49 YAML parse error: yaml: unmarshal errors:
line 5: mapping key "1" already defined at line 4
exit status 1
错误信息说 "1" 这个 key 已经存在了。那咱们先注释掉 "1": 3 试试:
yamlExample := ` object: a: 1 1: 2 # "1": 3 key: value array: - null_value: - boolean: true - integer: 1 `
再次运行,这次成功了:
$ go run main.go
Type: map[string]interface {}
Value: map[string]interface {}{"object":map[interface {}]interface {}{"a":1, "array":[]interface {}{map[string]interface {}{"null_value":interface {}(nil)}, map[string]interface {}{"boolean":true}, map[string]interface {}{"integer":1}}, "key":"value", 1:2}}
--------------------------
Type: []uint8
Value: {"object":{"1":2,"a":1,"array":[{"null_value":null},{"boolean":true},{"integer":1}],"key":"value"}}
可以看到,在 yaml.v3 中,1 和 "1" 会被当作同一个 key 处理,导致冲突。另外,细心的读者可能注意到了,代码里 yaml.Unmarshal 出来的 data 并没有直接交给 json.Marshal,而是先经过 dyno.ConvertMapI2MapS(data) 转换了一遍。为什么需要这一步?这是 Go YAML 转 JSON 时必须注意的 map 类型转换细节。
如果去掉这个转换,直接写:
package main
import (
"encoding/json"
"fmt"
"log"
"gopkg.in/yaml.v3" // YAML 解析库
)
func main() {
yamlExample := `
object:
a: 1
1: 2
# "1": 3
key: value
array:
- null_value:
- boolean: true
- integer: 1
`
// 解析 YAML
var data interface{}
if err := yaml.Unmarshal([]byte(yamlExample), &data); err != nil {
log.Fatalf("YAML parse error: %v", err)
}
fmt.Printf("Type: %TnValue: %#vn", data, data)
fmt.Println("--------------------------")
// 转换为 JSON
jsonData, err := json.Marshal(data)
if err != nil {
log.Fatalf("JSON convert error: %v", err)
}
fmt.Printf("Type: %TnValue: %sn", jsonData, jsonData)
}
运行结果:
$ go run main.go
Type: map[string]interface {}
Value: map[string]interface {}{"object":map[interface {}]interface {}{"a":1, "array":[]interface {}{map[string]interface {}{"null_value":interface {}(nil)}, map[string]interface {}{"boolean":true}, map[string]interface {}{"integer":1}}, "key":"value", 1:2}}
--------------------------
2025/07/31 23:34:26 JSON convert error: json: unsupported type: map[interface {}]interface {}
exit status 1
这次报错换成了 json.Marshal 不支持 map[interface {}]interface {} 类型。把反序列化出来的对象整理一下,就能看清楚:
map[string]interface{}{
"object": map[interface{}]interface{}{
"a": 1,
"array": []interface{}{
map[string]interface{}{
"null_value": interface{}(nil),
},
map[string]interface{}{
"boolean": true,
},
map[string]interface{}{
"integer": 1,
},
},
"key": "value",
1: 2,
},
}
问题出在 map[interface {}]interface {} 这个类型上。Go 的 map 允许任何可比较类型作为 key,但 JSON 的 key 必须是字符串。所以需要一个转换函数,把 map[interface {}]interface {} 变成 map[string]interface {}。dyno.ConvertMapI2MapS 就是干这个的,它递归地将所有 interface{} 类型的键转换为字符串,是 Go YAML 转 JSON 时的必备工具:
// ConvertMapI2MapS walks the given dynamic object recursively, and
// converts maps with interface{} key type to maps with string key type.
// This function comes handy if you want to marshal a dynamic object into
// JSON where maps with interface{} key type are not allowed.
//
// Recursion is implemented into values of the following types:
// -map[interface{}]interface{}
// -map[string]interface{}
// -[]interface{}
//
// When converting map[interface{}]interface{} to map[string]interface{},
// fmt.Sprint() with default formatting is used to convert the key to a string key.
func ConvertMapI2MapS(v interface{}) interface{} {
switch x := v.(type) {
case map[interface{}]interface{}: // 目标转换类型,需要把 key 为 interface{} 类型的转换成 string
m := map[string]interface{}{}
for k, v2 := range x {
switch k2 := k.(type) {
case string: // 如果 key 已经是 string 类型,则直接使用
m[k2] = ConvertMapI2MapS(v2)
default: // 如果 key 是其他类型则需要转换成 string
m[fmt.Sprint(k)] = ConvertMapI2MapS(v2)
}
}
v = m
case []interface{}: // 递归处理数组元素
for i, v2 := range x {
x[i] = ConvertMapI2MapS(v2)
}
case map[string]interface{}: // key 已经是 string,仅递归处理 value
for k, v2 := range x {
x[k] = ConvertMapI2MapS(v2)
}
}
return v
}
代码逻辑很清晰:递归遍历,把 map[interface{}]interface{} 的 key 转成字符串。这个工具在 Go YAML 解析和 JSON 序列化之间起到了关键的桥梁作用。
那么,为什么 yaml.Unmarshal 不直接返回 map[string]interface{} 呢?其实在 yaml.v3 的 issues 里有人问过,作者的回答是:
Unfortunately not.. Unlike json, yaml can take keys of arbitrary types.
因为 YAML 规范允许任意类型的值作为对象的 key,所以 1: 2 和 "1": 3 同时存在在 YAML 中是合法的。因此,在 Go 里做 YAML 转 JSON 时,保险起见一定要加上 dyno.ConvertMapI2MapS,避免直接把 map[interface{}]interface{} 丢给 json.Marshal。这是 Go 语言 YAML 转 JSON 的经典踩坑点。

顺便提一下,gopkg.in/yaml.v3 是 Canonical 公司(Ubuntu 背后的公司)为支持 Juju 项目开发的社区库,也是 Go 社区最常用的 YAML 解析库。不过,这个项目在 2025 年 4 月 2 日已被作者标记为不再维护。为此,我特意测试了另一个 Go YAML 包——go-yaml,看看效果如何。
使用 go-yaml 包处理 YAML
go-yaml 的用法和 yaml.v3 几乎一模一样,只需要改一下 import:
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/goccy/go-yaml" // YAML 解析库
)
func main() {
yamlExample := `
object:
a: 1
1: 2
"1": 3
key: value
array:
- null_value:
- boolean: true
- integer: 1
`
// 解析 YAML
var data interface{}
if err := yaml.Unmarshal([]byte(yamlExample), &data); err != nil {
log.Fatalf("YAML parse error: %v", err)
}
fmt.Printf("Type: %TnValue: %#vn", data, data)
fmt.Println("--------------------------")
// 转换为 JSON
jsonData, err := json.Marshal(data)
if err != nil {
log.Fatalf("JSON convert error: %v", err)
}
fmt.Printf("Type: %TnValue: %sn", jsonData, jsonData)
}
运行结果:
$ go run goccy-go-yaml/main.go
2025/07/31 23:25:15 YAML parse error: [5:3] mapping key "1" already defined at [4:3]
2 | object:
3 | a: 1
4 | 1: 2
> 5 | "1": 3
^
6 | key: value
7 | array:
8 | - null_value:
exit status 1
和 yaml.v3 一样,它也无法处理同时存在 1: 2 和 "1": 3 的情况。不过报错信息更清晰,直接指出了具体行号。同样注释掉 "1": 3 后,就成功了:
$ go run goccy-go-yaml/main.go
Type: map[string]interface {}
Value: map[string]interface {}{"object":map[string]interface {}{"1":0x2, "a":0x1, "array":[]interface {}{map[string]interface {}{"null_value":interface {}(nil)}, map[string]interface {}{"boolean":true}, map[string]interface {}{"integer":0x1}}, "key":"value"}}
--------------------------
Type: []uint8
Value: {"object":{"1":2,"a":1,"array":[{"null_value":null},{"boolean":true},{"integer":1}],"key":"value"}}
有趣的是,go-yaml 直接返回了 map[string]interface{},不需要再借助 dyno.ConvertMapI2MapS 做二次转换。也就是说,go-yaml 一个包就能搞定 yaml.v3 + dyno 两个包才能完成的事。对于 Go YAML 转 JSON 的场景,go-yaml 提供了更简洁的解决方案。
到此,Go 里 YAML 转 JSON 的坑就讲完了。不过文章还没完——我用 Python 处理同样的 YAML 文档,看看为什么我当时会疏忽这个问题。
Python 中处理 YAML to JSON
Python 代码:
import json
import yaml
yaml_example = """
object:
a: 1
1: 2
"1": 3
key: value
array:
- null_value:
- boolean: true
- integer: 1
"""
def main():
y = yaml.load(yaml_example, Loader=yaml.SafeLoader)
print(type(y), y)
print("--------------------------")
j = json.dumps(y)
print(type(j), j)
print("--------------------------")
d = json.loads(j)
print(type(d), d)
if __name__ == '__main__':
main()
运行结果:
$ python main.py
{'object': {'a': 1, 1: 2, '1': 3, 'key': 'value', 'array': [{'null_value': None}, {'boolean': True}, {'integer': 1}]}} --------------------------
{"object": {"a": 1, "1": 2, "1": 3, "key": "value", "array": [{"null_value": null}, {"boolean": true}, {"integer": 1}]}} --------------------------
{'object': {'a': 1, '1': 3, 'key': 'value', 'array': [{'null_value': None}, {'boolean': True}, {'integer': 1}]}}
Python 完美地处理了 1: 2 和 "1": 3 同时存在的场景。注意,json.dumps 序列化出来的字符串里同时出现了两个 "1",但再用 json.loads 反序列化时,只会保留最后一个 '1': 3,因为 JSON 规范要求 key 必须是字符串,重复的 key 会被覆盖。这也体现了 Python 处理 YAML 转 JSON 时的灵活性。
所以你看,同样的 YAML 文档,在 Python 里没问题,在 Go 里就踩坑了。这也是我喜欢 Python 的一点——足够灵活。顺便说一句,Python 虽然灵活,但它是动态强类型语言,不是弱类型,而 Go 是静态强类型语言。类型系统的差异直接导致了 YAML 转 JSON 时处理方式的不同。
原因分析
演示了这么多,YAML 转 JSON 报错的问题,归根结底是 YAML 和 JSON 格式的差异造成的。YAML 是 JSON 的超集,任何合法的 JSON 文档都是合法的 YAML,但反过来不成立——不是所有合法的 YAML 都是合法的 JSON。理解这一点对于正确处理 Go YAML 转 JSON 至关重要。
YAML 允许数字作为对象的 key,这点和 Python 的 dict 完美契合,但和 Go 的严格类型系统存在冲突。所以,同样的 YAML 文档,在 Python 里处理得顺风顺水,在 Go 里就报错了。
问题虽然解决了,但还想再扩展一下关于 YAML 和 JSON 规范的知识,毕竟知其然更要知其所以然。
YAML 规范
YAML 规范官方文档见 yaml.org。这里有一个挺有意思的点:
A question mark and space (“
?”) indicate a complex mapping key. Within a block collection, key/value pairs can start immediately following the dash, colon or question mark.
简单说,问号加空格(? )在 YAML 里表示一个复杂的 mapping 键。举个例子:
? - Detroit Tigers - Chicago cubs : - 2001-07-23
这是一个合法的 YAML 文档。? 后面的内容(列表)是 key,: 后面的内容是 value。
另一个例子:
? [ New York Yankees,
Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
2001-08-14 ]
key 和 value 都是数组。不过很遗憾,Go 和 Python 的 YAML 库都不支持这种复杂键。但 JavaScript 的 js-yaml 可以:
const yaml = require('js-yaml');
const yamlExample = `
? - Detroit Tigers
- Chicago cubs
: - 2001-07-23
? [ New York Yankees,
Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
2001-08-14 ]
`;
try {
const data = yaml.load(yamlExample);
console.log(data);
} catch (e) {
console.error(e);
}
解析结果:
{
'Detroit Tigers,Chicago cubs': [ 2001-07-23T00:00:00.000Z ],
'New York Yankees,Atlanta Braves': [
2001-07-02T00:00:00.000Z,
2001-08-12T00:00:00.000Z,
2001-08-14T00:00:00.000Z
]
}
JavaScript 用了一种变通方式——把数组 key 转成逗号分隔的字符串。这也展示了不同语言对 YAML 规范的支持差异。
JSON 规范
JSON(JavaScript Object Notation)由道格拉斯·克罗克福特设计,是 JavaScript 的子集,也是目前主流编程语言之间数据交换的事实标准。维基百科对 JSON 对象的描述中有一句非常重要的话:
对象:若干无序的“键-值对”(key-value pairs),其中键只能是字符串[2]。建议但不强制要求对象中的键是独一无二的。
关键点就是“键只能是字符串”,这一点要牢记。当你进行 Go YAML 转 JSON 时,必须确保所有键都符合 JSON 规范。
总结
本文通过一个实际案例,记录了 Go 里 YAML 转 JSON 时遇到的坑,并给出了解决方案。同时,简单介绍了 YAML 和 JSON 规范,希望能让大家知其然并知其所以然。这个问题很小,甚至不值一提,几分钟就能搞定,但它背后折射出的是对官方标准规范的忽视——还是倡导大家多读官方文档。
回想编程刚入门的时候,其实很怕看官方文档,觉得晦涩难懂,但很多细节就藏在标准规范里,这是绕不过去的门槛,没有捷径可走。最后,单元测试真的很重要,充分测试你的 YAML 转 JSON 代码,有助于减少 Bug——虽然无法完全避免 :)
好了,本文就到这里。更多关于 YAML 和 JSON 规范的内容,可以到官网查看学习。希望这篇 Go YAML 转 JSON 踩坑笔记能帮你在实际开发中少走弯路。
