Go泛型类型实现嵌套数组JSON反序列化时的问题及解决方案咨询
我遇到了一个API返回格式的痛点:这个API在很多地方都会把数组包装成一个包含number字段和数组字段的对象,比如:
{ "items": { "number": 3, "item": [ { ... } // 具体内容无关 ] } }
而且每个地方的数组字段名还不一样,但可以确定的是,这种包装对象里只有两个键:一个是number,另一个就是我们需要的数组。
这种结构让Go结构体的使用变得很麻烦,每次都要多一层不必要的字段访问。我希望能直接把这种结构映射成更简洁的格式,比如:
{ "items": [ { ... } // 具体内容无关 ] }
一开始我想过给每个涉及的结构体都写自定义的UnmarshalJSON方法,但这种重复劳动太繁琐了——毕竟这种嵌套结构几乎出现在每个结构体里。所以我打算用泛型来实现一个通用的类型,自动处理这种嵌套数组的解析。
我的尝试代码如下:
// NestedArray 用于将不必要嵌套的数组向上提取 type NestedArray[T any] []T func (n *NestedArray[T]) UnmarshalJSON(bytes []byte) error { // 先反序列化成map target := make(map[string]interface{}) err := json.Unmarshal(bytes, &target) if err != nil { return err } // 找到嵌套的数组(键名未知,通过类型判断) var sliceVal interface{} for k, v := range target { if k == "number" { continue } rt := reflect.TypeOf(v) if rt.Kind() == reflect.Slice { sliceVal = v break } } // 找不到数组或者为空,直接设为nil if sliceVal == nil { *n = nil return nil } // 转回JSON再解析到目标类型 sliceJSON, err := json.Marshal(sliceVal) if err != nil { return err } err = json.Unmarshal(sliceJSON, n) // 错误发生在这里 if err != nil { return err } return nil }
使用方式如下:
type Item struct { // 具体字段无关 } type Root struct { // 使用泛型类型将JSON对象解析为嵌套的数组 Items NestedArray[Item] `json:"items,omitempty"` }
但运行后出现了错误:
json: cannot unmarshal array into Go struct field Root.items of type map[string]interface{}
我用调试器看了下,sliceVal确实是我想要的数组,但在把它反序列化回NestedArray[T]的时候报错了。想请教下这个问题的解决方案是什么?有没有比我当前思路更优的实现方式?
问题根源分析
你遇到的错误本质是自定义UnmarshalJSON方法的递归调用:当你在方法内部执行json.Unmarshal(sliceJSON, n)时,n是*NestedArray[T]类型,而这个类型已经实现了UnmarshalJSON方法,所以json包会再次调用你写的这个方法。但此时sliceJSON是一个数组格式的JSON,而你的方法第一步是把它反序列化成map[string]interface{},这显然会失败——数组无法被解析成map,于是就出现了你看到的错误。
解决方案
我们需要绕过递归调用,直接把sliceVal解析成[]T类型,再赋值给NestedArray[T]。这里有两种可行的实现方式:
方案1:临时变量绕过自定义反序列化
既然NestedArray[T]本质是[]T的自定义类型,我们可以先把数组JSON解析到一个临时的[]T变量,再把它转换成NestedArray[T]赋值给目标:
func (n *NestedArray[T]) UnmarshalJSON(bytes []byte) error { target := make(map[string]interface{}) err := json.Unmarshal(bytes, &target) if err != nil { return err } var sliceVal interface{} for k, v := range target { if k == "number" { continue } if rv, ok := v.([]interface{}); ok { sliceVal = rv break } } if sliceVal == nil { *n = nil return nil } // 用临时变量[]T来接收,绕过自定义的UnmarshalJSON var temp []T sliceJSON, err := json.Marshal(sliceVal) if err != nil { return err } err = json.Unmarshal(sliceJSON, &temp) if err != nil { return err } // 转换为NestedArray[T]赋值 *n = NestedArray[T](temp) return nil }
方案2:直接通过反射转换(更高效)
上面的方案需要先序列化再反序列化,有点多余。我们可以利用反射直接把[]interface{}转换成[]T,避免JSON的二次处理:
func (n *NestedArray[T]) UnmarshalJSON(bytes []byte) error { target := make(map[string]interface{}) err := json.Unmarshal(bytes, &target) if err != nil { return err } var slice []interface{} for k, v := range target { if k == "number" { continue } var ok bool slice, ok = v.([]interface{}) if ok { break } } if slice == nil { *n = nil return nil } // 利用反射创建[]T类型的切片 t := reflect.TypeOf(*n).Elem() // 获取T的类型 result := reflect.MakeSlice(reflect.SliceOf(t), len(slice), len(slice)) for i, item := range slice { // 将每个interface{}元素转换为T类型 itemJSON, err := json.Marshal(item) if err != nil { return err } var elem T err = json.Unmarshal(itemJSON, &elem) if err != nil { return err } result.Index(i).Set(reflect.ValueOf(elem)) } // 将反射切片赋值给*n *n = result.Interface().(NestedArray[T]) return nil }
额外优化建议
- 可以增加对非对象情况的兼容:如果API返回的本身就是数组(而不是包装对象),你的当前代码会失败。可以在开头先尝试直接反序列化成
[]T,失败了再走嵌套解析的逻辑。 - 可以给
NestedArray[T]实现MarshalJSON方法,确保序列化时能转回API要求的格式(如果需要双向处理的话)。
备注:内容来源于stack exchange,提问作者stijndcl

