跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

处理 JSON 中可能为对象或数组的动态字段

本文介绍在 Go 中如何优雅处理第三方 API 返回的 JSON 字段——该字段有时是单个对象,有时是对象数组,避免因结构体类型不匹配导致 json.Unmarshal 失败。核心方案是结合 json.RawMessage 延迟解析与自定义 UnmarshalJSON 方法实现类型自适应。 本文介绍在 go 中如何优雅处理第三方 api 返回的 json 字段——该字段有时是单个对象,有时是对象数组,避免因结构体类型不匹配导致 `json.unmarshal` 失败。核心方案是结合 `json.rawmessage` 延迟解析与自定义 `unmarshaljson` 方法实现类型自适应。 在实际开发中,尤其是对接不规范或版本混杂的第三方 REST API(如家庭电表监控服务)时,常遇到同一字段名(如 "gwrcmd")在不同请求场景下返回不同类型 JSON 数据的问题: 查询智能电表列表时,"gwrcmd" 是单个对象; 查询用电图表数据时,"gwrcmd" 却是一个对象数组。 若强行用固定结构体(如始终定义为 struct{} 或 []struct{})解析,encoding/json 会直接返回 json: cannot unmarshal object/array into Go struct 类错误。硬编码两套结构体并重复解析逻辑不仅冗余,还严重损害可维护性。 ✅ 推荐方案:json.RawMessage + 自定义反序列化 Go 标准库提供的 json.RawMessage 是解决此类“类型不确定”问题的标准范式。它本质是 []byte 的别名,能 零拷贝暂存原始 JSON 字节流 ,延迟到运行时根据内容特征决定最终解析目标。 以下是以你提供的电表 API 为例的完整实现:
type Response struct { Gwrcmds struct { RawGwrcmd json.RawMessage `json:"gwrcmd"` // 注意:此处不直接定义 Gwrcmd 字段,避免冲突 } `json:"gwrcmds"` // 两个互斥字段:用于存放解析结果 Gwrcmd *GwrcmdItem `json:"-"` // 单个对象(指针便于判空) Gwrcmds []GwrcmdItem `json:"-"` // 数组对象 } type GwrcmdItem struct { Gcmd string `json:"gcmd"` Gdata struct { Gip struct { Version string `json:"version"` } `json:"gip"` } `json:"gdata"` } // 实现自定义 UnmarshalJSON 方法 func (r *Response) UnmarshalJSON(data []byte) error { // 第一步:先解析外层结构,仅提取 raw gwrcmd 字段 type Alias Response // 防止递归调用 aux := &struct { Gwrcmds struct { RawGwrcmd json.RawMessage `json:"gwrcmd"` } `json:"gwrcmds"` *Alias }{ Alias: (*Alias)(r), } if err := json.Unmarshal(data, &aux); err != nil { return err } raw := aux.Gwrcmds.RawGwrcmd if len(raw) == 0 { return nil // 空值容错 } // 第二步:根据首字符判断是对象 '{}' 还是数组 '[' switch raw[0] { case '{': var item GwrcmdItem if err := json.Unmarshal(raw, &item); err != nil { return fmt.Errorf("failed to unmarshal gwrcmd as object: %w", err) } r.Gwrcmd = &item case '[': var items []GwrcmdItem if err := json.Unmarshal(raw, &items); err != nil { return fmt.Errorf("failed to unmarshal gwrcmd as array: %w", err) } r.Gwrcmds = items default: return fmt.Errorf("unexpected JSON type for gwrcmd: %q", raw[:min(10, len(raw))]) } return nil }
? 关键技巧说明 : 使用 type Alias Response 避免在自定义 UnmarshalJSON 中无限递归调用自身; json:"-" 标签确保 Gwrcmd 和 Gwrcmds 不参与默认解析,完全由我们控制; min(10, len(raw)) 是安全截断,防止日志打印过长原始数据。 ⚠️ 注意事项与进阶建议 首字节判断的局限性 :虽然 raw[0] == '[' 或 '{' 在绝大多数标准 JSON 中可靠,但若 API 返回带 BOM 或空白符(如 \n {),需先 bytes.TrimSpace()。更健壮的做法是借助 json.Token 流式探测类型。 语义化提示优于启发式判断 :理想情况下,应推动 API 提供明确类型标识字段(如 "gwrcmd_type": "single" 或 "gwrcmd_count": 1)。若有此类字段,优先基于其值分支解析,而非依赖 JSON 文本形态。 性能考量 :json.RawMessage 避免了中间解析开销,但每次 Unmarshal 仍需两次解码(一次取 raw,一次转目标)。对高频调用场景,可考虑缓存 json.Decoder 实例复用。 错误处理必须显式 :切勿忽略 json.Unmarshal 错误。示例中使用 fmt.Errorf(... %w) 包装错误,保留原始堆栈,利于调试。 ✅ 总结 面对 JSON 字段类型动态变化的场景, 不要复制结构体,也不应依赖 interface{} 弱类型做运行时断言 。正确路径是: 用 json.RawMessage 暂存原始数据; 通过轻量探测(首字符 / token / 辅助字段)识别真实类型; 调用对应 json.Unmarshal 到精确结构体。 该模式兼具类型安全、可读性与扩展性,是 Go 生态中处理“柔性 JSON Schema”的事实标准实践。

相关文章