用datatypes.json是最省心的方案:gorm v2.2.5+内置该类型,底层为[]byte别名,自动实现scanner/valuer接口,支持结构体、map、slice直接读写,避免cannot unmarshal []byte等常见panic,但无类型约束,强校验需业务层补充。

用 datatypes.JSON 是最省心的方案
新版本 GORM(v2.2.5+)内置了 gorm.io/datatypes 包,其中 datatypes.JSON 是专为 MySQL/PostgreSQL 原生 JSON 字段设计的类型。它底层是 []byte 别名,自动实现 Scanner 和 Valuer 接口,无需手写序列化逻辑。
常见错误现象:直接用 string 或 interface{} 映射 JSON 字段,导致读取时 panic 报 cannot unmarshal []byte into Go struct field X;或写入空结构体变成 "{}" 而非数据库 NULL。
- 模型定义示例:
type User struct { ID uint; Profile datatypes.JSON } - 写入时传结构体、map 或 slice,GORM 自动
json.Marshal成[]byte存库 - 读取时自动
json.Unmarshal到目标类型,支持直接赋值给map[string]interface{}或自定义 struct - 注意:
datatypes.JSON本身不带类型约束,若需强校验,应在业务层做json.Unmarshal后检查字段
gorm:"serializer:json" 标签适合结构体字段
当你有一个固定结构的 JSON 字段(比如 BankCardInfo),且希望 GORM 直接帮你完成结构体 ↔ JSON 的双向转换,用 serializer:json 标签比 datatypes.JSON 更语义清晰、类型安全。
使用场景:字段含义明确、结构稳定、需要编译期类型检查(如 IDE 提示、字段补全)。
- 定义方式:
Milestones BankCardInfo `gorm:"serializer:json"` - GORM v2 会自动调用
json.Marshal/json.Unmarshal,无需额外接口实现 - 若字段可能为
NULL,建议用指针类型:*BankCardInfo,否则反序列化空 JSON 会 panic - 不推荐用于深度嵌套或动态结构,因为每次变更都要改 Go struct,失去 JSON 的灵活性
手动实现 Scanner + Valuer 最可控
这是兼容性最强、生产环境最稳妥的做法,尤其适用于老项目迁移、需要精细控制 NULL 处理、或字段内容需加解密/压缩等定制逻辑的场景。
容易踩的坑:忽略 nil 检查,导致数据库存入 "{}";或未处理空 []byte,反序列化时报错;或在 Value() 中忘记返回 nil 表示 SQL NULL。
- 关键点:在
Scan()中先判断value == nil或len([]byte) == 0,再决定是否反序列化 - 在
Value()中,若 Go 值为nil或空结构体,应返回nil(让数据库存NULL),而非json.Marshal({}) - 性能影响:相比
datatypes.JSON,手动实现无额外开销,甚至可复用bytes.Buffer减少内存分配 - 该方案完全兼容 GORM v1/v2,且不依赖任何第三方包
别用 string 直接存 JSON 字符串
虽然能跑通,但这是最容易埋雷的方式。MySQL 原生 JSON 类型的优势(校验、函数支持、索引能力)全部丢失,等于退化回 TEXT 字段。
典型问题:前端传一个少了个逗号的 JSON,后端 json.Unmarshal 失败,但数据已落库;后续查询用 JSON_EXTRACT 时直接报错;无法对 JSON 内部字段建生成列索引。
- 如果必须用
string,至少加一层封装:定义类型type JSONString string,并强制实现Scanner/Valuer,保证入库前做json.Valid()校验 - 千万别在
WHERE条件里写WHERE attrs LIKE '%key%'—— 这种模糊匹配既慢又不可靠,应改用JSON_CONTAINS(attrs, '"value"', '$.key') - 长期看,只要数据库是 MySQL 5.7+ 或 PostgreSQL,就该用原生 JSON 类型,而不是把 JSON 当字符串哄着用
真正麻烦的从来不是“怎么存”,而是“怎么查得准、分页不崩、改起来不卡”。JSON 字段一旦上了生产,字段内部结构的松散性会迅速传导到查询逻辑和索引策略上——比如 JSON_EXTRACT 结果没法直接走普通索引,得靠生成列+函数索引配合,这点很容易被忽略。











