症状
这个错误是一行写完的事故报告:json: cannot unmarshal string into Go struct field Project.tags of type []string——同时点名负载类型(string)、结构体字段路径(Project.tags)与声明的 Go 类型([]string)。下一步是调查真实负载的形状分布:jq 统计发现 812 个样本是数组、31 个是逗号字符串——生产者改过约定(或从来没有约定),两种形状都在发,只有少数派失败。这也是“部分 payload 报错、部分正常”的原因。
诊断思路
先从报错读出字段路径与类型对,立即定位修复点;再统计该字段在样本中的全部实际形状,决定是改标签就够还是需要自定义 Unmarshaler;最后检查结构体标签拼写与缺失——拼错或漏写的 json 标签会把 Go 字段绑到错误的键上,在邻近字段制造类型失配。
根因
encoding/json 要求 JSON 值类型与 Go 字段类型精确对应(除少数已知强制转换)。某字段以不同类型到达时,整个 Unmarshal 以字段专属消息失败。
修复与验证
真实联合类型字段用自定义类型接管解析:为 StringList 实现 UnmarshalJSON,先试单值再试数组,分别处理两种形状;变体需要先读鉴别字段的负载,用 json.RawMessage 延迟解析,等 Kind 明确后再解构;无趣的情况用标签修正——对齐名称、生产者省略时配 omitempty、负载不携带的字段标 json:“-”。验证时把真实样本语料全部过一遍 Unmarshal,断言零错误且每种已观察的字段形状都得到正确值。把字段一律改成 interface{} 来消错会丢掉类型安全、把失败推迟到运行期属性访问——用有针对性的自定义类型。
代码示例
json: cannot unmarshal string into Go struct field
Project.tags of type []string
jq '.tags' samples/*.json | sort | uniq -c
# 812 ["go","json"]
# 31 "go,json" <- the producer sometimes flattens to a comma string
type StringList []string
func (s *StringList) UnmarshalJSON(b []byte) error {
var single string
if err := json.Unmarshal(b, &single); err == nil {
*s = strings.Split(single, ",")
return nil
}
var many []string
if err := json.Unmarshal(b, &many); err != nil {
return fmt.Errorf("StringList: %w", err)
}
*s = many
return nil
}
type Project struct {
Tags StringList `json:"tags"`
}
type Event struct {
Kind string `json:"kind"`
Data json.RawMessage `json:"data"` // parse later, once Kind is known
}