gorm中json字段必须用json.rawmessage或自定义类型,因其作为字节流暂存可避免序列化错乱、类型丢失和空值异常;string或map[string]interface{}会导致解析失真,且gorm仅透传不解析,需go层主动处理。

JSON字段在GORM中必须用json.RawMessage或自定义类型才能安全读写
直接用string或map[string]interface{}存JSON,会导致序列化/反序列化错乱、丢失类型信息、空值处理异常。GORM对JSON没有原生透明支持,它只做“透传”,真正解析责任在Go层。
推荐方案是用json.RawMessage——它把JSON当作字节流暂存,不提前解析,避免中间态转换错误。如果需要结构化访问,再按需json.Unmarshal到具体struct。
-
json.RawMessage字段必须配type: json标签,否则GORM会当普通文本处理(比如存成带引号的字符串) - 数据库列类型要匹配:MySQL用
JSON,PostgreSQL用JSONB,SQLite用TEXT(但需手动确保内容合法) - 别用
sql.NullString套JSON,它无法处理嵌套结构,且Valid字段对JSON空对象{}和null区分无力
GORM迁移时JSON字段不能靠AutoMigrate自动建出正确类型
AutoMigrate对json.RawMessage字段默认生成TEXT类型,即使你写了type: json标签。MySQL 5.7+ 和 PostgreSQL 要显式指定类型,否则后续JSON_CONTAINS等函数会失效或报错。
正确做法是在AutoMigrate后,用db.Migrator().AlterColumn或原生SQL修正类型:
// MySQL
db.Exec("ALTER TABLE users MODIFY COLUMN metadata JSON")
// PostgreSQL
db.Exec("ALTER TABLE users ALTER COLUMN metadata TYPE JSONB USING metadata::jsonb")
- SQLite不支持JSON函数,
type: json仅作语义提示,实际仍靠应用层校验 - 如果表已存在且含脏数据(如非JSON字符串),
ALTER COLUMN可能失败,需先清洗 - 别依赖
gorm.Model里的ColumnTypes判断是否为JSON类型——它返回的是驱动层面的类型名(如varchar),不是逻辑类型
查询JSON字段内嵌值要用数据库原生函数,GORM不翻译map或struct字段路径
GORM不会把user.Metadata.Name这种写法转成metadata->>'name'。想查JSON内部字段,必须手写表达式,用Where或Select传入原生SQL片段。
例如查MySQL中metadata里status为"active"的记录:
var users []User
db.Where("metadata->>'$.status' = ?", "active").Find(&users)
- PostgreSQL用
metadata->>'status'(取text)或metadata->'status'(取json);MySQL用metadata->>'$.status',注意$前缀 - 用
SELECT投影JSON子字段时,别漏掉别名:db.Select("id, metadata->>'$.email' as email"),否则Scan会找不到字段 - WHERE里用
JSON_CONTAINS查数组包含(MySQL)或@>操作符(PostgreSQL)时,参数必须是合法JSON字符串,不能直接传Go slice
更新JSON字段部分键值不能只改struct再全量写回,否则会覆盖其他字段
如果用json.RawMessage字段+完整struct解码→修改某字段→重新json.Marshal→存回,等于“读-改-写”全量替换,高并发下极易丢变更。正确方式是用数据库的JSON函数原地更新。
例如只把metadata里的last_login设为当前时间,不碰其他字段:
// MySQL
db.Exec("UPDATE users SET metadata = JSON_SET(metadata, '$.last_login', ?) WHERE id = ?", time.Now().Format(time.RFC3339), 123)
// PostgreSQL
db.Exec("UPDATE users SET metadata = jsonb_set(metadata, '{last_login}', ?) WHERE id = ?", "\""+time.Now().Format(time.RFC3339)+"\"", 123)
- MySQL的
JSON_SET如果路径不存在会创建,JSON_REPLACE则只更新已有路径,选哪个取决于业务语义 - PostgreSQL的
jsonb_set第三个参数必须是JSON格式字符串(如"\"value\""),不是裸字符串 - 不要在事务外执行这类语句——JSON函数不是原子的,若中间出错,状态可能不一致
JSON字段看着灵活,实则每一步都在边界上走:序列化时机、数据库类型、查询语法、更新粒度,四个环节只要一个没对齐,数据就 quietly 损坏。最稳妥的做法是——能不用就不用,非用不可时,全程控制解析权,别交给GORM或driver猜。











