Go语言自定义Unmarshal实现扁平JSON转嵌套结构体
问题根因
missing destination name deadline报错的原因很明确:你拿到的输入(不管是扁平JSON还是数据库返回的扁平行)里,deadline等时间字段是和todo_id、todo_name这类字段平级的,但是你定义的结构体里这些时间字段属于嵌套的Parsed_Time类型,默认的反序列化/结构体扫描逻辑找不到根级别的对应字段接收值,就会抛错。
完全可以通过自定义解析逻辑实现单次请求完成所有字段赋值,不需要两次查库。
实现方案
方案1:实现自定义UnmarshalJSON接口(推荐,性能最优)
Go的encoding/json包原生支持自定义类型的反序列化逻辑,只要给你的Todo结构体实现UnmarshalJSON([]byte) error方法,反序列化的时候就会自动走你的自定义逻辑,刚好匹配你要的「先初始化带空Times的Todo,再单独解析时间字段」的需求。
核心逻辑分两步:
- 临时移除结构体里的
Times字段,先把所有平级的非时间字段解析到Todo实例,这一步Times会保持零值初始化 - 把同一份扁平输入单独解析到
Parsed_Time实例,赋值给Todo的Times字段
基础结构体定义(可替换为实际业务字段):
type Sub struct { // 填写Sub结构体的实际业务字段 } type Parsed_Time struct { Deadline int64 `json:"deadline"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` // 其他需要从扁平结构提取的时间字段 } type Todo struct { Todo_id int `json:"todo_id" db:"todo_id"` Todo_name string `json:"todo_name" db:"todo_name"` User_id int `json:"user_id" db:"user_id"` Subs []Sub `json:"subs" db:"subs"` Times Parsed_Time `json:"times" db:"times"` }
自定义反序列化方法实现:
func (t *Todo) UnmarshalJSON(data []byte) error { // 定义别名结构体,临时去掉Times字段,避免递归调用自定义UnmarshalJSON导致栈溢出 type todoAlias struct { Todo_id int `json:"todo_id"` Todo_name string `json:"todo_name"` User_id int `json:"user_id"` Subs []Sub `json:"subs"` } // 第一步:解析非时间字段 var alias todoAlias if err := json.Unmarshal(data, &alias); err != nil { return err } // 把解析结果赋值给当前Todo实例 t.Todo_id = alias.Todo_id t.Todo_name = alias.Todo_name t.User_id = alias.User_id t.Subs = alias.Subs // 第二步:从同一份扁平JSON里解析时间字段 var times Parsed_Time if err := json.Unmarshal(data, ×); err != nil { return err } t.Times = times return nil }
如果你是用sqlx等ORM直接扫描数据库返回的扁平结果集报这个错,逻辑完全一致:给Todo实现sql.Scanner接口,内部逻辑同样是先扫描非Times字段,再单独扫描时间字段赋值给Times,不需要发起两次数据库查询。
方案2:用RawMessage中转解析(适合字段频繁变动的场景)
如果不想维护别名结构体,也可以先用json.RawMessage把所有字段承接为原始字节,按需挑字段分两次解析,缺点是需要手动维护字段列表,性能略低于方案1,示例代码:
func (t *Todo) UnmarshalJSON(data []byte) error { // 先把所有字段解析为原始RawMessage rawMap := make(map[string]json.RawMessage) if err := json.Unmarshal(data, &rawMap); err != nil { return err } // 组装非时间字段的原始数据 type tempTodo Todo // 用类型别名避免递归调用自定义Unmarshal var nonTimeTemp tempTodo nonTimeBytes, _ := json.Marshal(struct { Todo_id json.RawMessage `json:"todo_id,omitempty"` Todo_name json.RawMessage `json:"todo_name,omitempty"` User_id json.RawMessage `json:"user_id,omitempty"` Subs json.RawMessage `json:"subs,omitempty"` }{ Todo_id: rawMap["todo_id"], Todo_name: rawMap["todo_name"], User_id: rawMap["user_id"], Subs: rawMap["subs"], }) if err := json.Unmarshal(nonTimeBytes, &nonTimeTemp); err != nil { return err } *t = Todo(nonTimeTemp) // 解析时间字段 var times Parsed_Time timeBytes, _ := json.Marshal(rawMap) if err := json.Unmarshal(timeBytes, ×); err != nil { return err } t.Times = times return nil }
避坑提示
- 自定义反序列化时一定要避免无限递归:不要直接在
UnmarshalJSON里对*Todo类型调用json.Unmarshal,要么用去掉了嵌套字段的别名结构体,要么用type xxx Todo的方式做类型中转绕开自定义方法。 - Go标准库
encoding/json默认就会忽略JSON里存在、结构体里没有定义的字段,不会报未知字段错误;如果你是用sqlx做StructScan报的错,不需要全局关闭严格扫描模式,用上面的自定义Scanner逻辑就能精准解决问题,不会影响其他查询的字段校验。
内容的提问来源于stack exchange,提问作者Aleksander Barsukov
相关产品推荐
相关产品推荐

