You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,再单独解析时间字段」的需求。
核心逻辑分两步:

  1. 临时移除结构体里的Times字段,先把所有平级的非时间字段解析到Todo实例,这一步Times会保持零值初始化
  2. 把同一份扁平输入单独解析到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, &times); 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, &times); 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:36:18