GORM不支持仅通过struct tag(如gorm:"type:jsonb")实现自定义类型映射,必须由Go类型实现driver.Valuer和sql.Scanner接口;因为type tag只影响建表DDL,不控制编解码逻辑,否则JSON等字段会以字符串字面量存取导致双引号嵌套等问题。

GORM 不支持直接在 struct tag 里写数据库类型(比如 gorm:"type:jsonb")来实现任意自定义数据类型映射——它只认内置类型和少数可插拔的扫描/值接口实现。真要映射非标类型(如 PostgreSQL 的 jsonb、MySQL 的 ENUM、自定义二进制格式),必须靠 Go 层面的类型实现 driver.Valuer 和 sql.Scanner 接口。
为什么不能只靠 struct tag?
GORM 的 gorm:"type:xxx" 只影响建表时生成的 DDL 类型,不改变 Go 值与数据库值之间的编解码逻辑。比如你写 gorm:"type:jsonb",GORM 仍会把字段当 string 处理,读写时不做 JSON 序列化/反序列化,结果就是存进去的是字符串字面量,不是真正的 JSON 对象。
常见错误现象:
- 存入
{"name":"foo"},查出来是"{"name":"foo"}"(多了一层引号) - 数据库里是
ENUM('active','inactive'),但 Go 中用string接收,GORM 不校验值合法性 - PostgreSQL 的
citext(大小写不敏感文本)被当成普通text,索引或 WHERE 条件失效
正确做法:实现 Valuer + Scanner 接口
以 PostgreSQL jsonb 为例,你需要定义一个 Go 类型并实现两个接口:
type JSONB map[string]interface{}
func (j JSONB) Value() (driver.Value, error) {
return json.Marshal(j)
}
func (j *JSONB) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return fmt.Errorf("cannot scan %T into JSONB", value)
}
return json.Unmarshal(b, j)
}
然后在模型中使用它:
type User struct {
ID uint `gorm:"primaryKey"`
Data JSONB `gorm:"type:jsonb"` // 这里 type 才起建表作用
}
关键点:
-
Value()控制写入前如何转成数据库可接受的值(通常是[]byte或基本类型) -
Scan()控制从数据库读出后如何还原成 Go 值(注意指针接收者) -
gorm:"type:xxx"仅用于 Migrator 建表,不影响编解码行为 - 如果类型需要兼容多种数据库(如同时支持 MySQL
JSON和 PGjsonb),得在Value()/Scan()里做方言判断
用 gorm.io/plugin/dbresolver 时要注意类型注册
如果你用了读写分离(dbresolver),且自定义类型依赖特定驱动特性(如 pgx 的 pgtype.JSONB),别直接用原生 database/sql 接口。GORM 默认走 database/sql,所以你的 Valuer/Scanner 必须基于标准库语义实现,不能强依赖 pgx 内部类型。否则在 resolver 切换连接时可能 panic。
容易踩的坑:
- 在
Scan()里直接断言value.(*pgtype.JSONB)—— 这在 GORM 主流程里根本不会发生 - 忘记处理
nil值(比如数据库字段为 NULL,value是nil,需显式返回nil) - 用
json.RawMessage替代自定义类型 —— 它虽自带接口实现,但无法做字段级验证或默认值注入
最常被忽略的是:自定义类型一旦引入,所有涉及该字段的查询条件(Where、Updates)都必须传入同类型实例,不能混用 map[string]interface{} 或 string,否则 GORM 无法调用你的 Value() 方法,会静默失败或报错 unsupported driver type。


















