PostgreSQL的jsonb类型本身自动拒绝非法JSON,触发器无需校验语法合法性,只需校验业务语义;应使用jsonb_path_exists()等函数做断言,并配合RAISE EXCEPTION拦截不合规数据。

触发器里怎么判断jsonb字段是否合法
PostgreSQL 的 jsonb 类型本身会自动拒绝非法 JSON 字符串(比如插入 '{a:1}' 会直接报错 invalid input syntax for type jsonb),所以你**不需要在触发器里重复校验语法合法性**。真正需要校验的是「业务语义」:比如要求某个键必须存在、值必须是字符串、数组长度不能超过 5 等。
触发器中用 jsonb_typeof()、jsonb_exists()、jsonb_path_exists() 这类函数做断言,配合 RAISE EXCEPTION 拦截不合规数据。
常见错误是误以为 jsonb_valid('...') 是内置函数——它并不存在,别白费劲去查文档。
用jsonb_path_exists()写可读性高的校验逻辑
jsonb_path_exists() 支持 SQL/JSON 路径表达式,比嵌套 ->> + IS NOT NULL 更简洁,也更容易表达复合条件(如“status 是 'active' 且 created_at 是 ISO8601 格式字符串”)。
示例:要求 payload 字段必须包含 user_id(整数)和 tags(非空字符串数组):
CREATE OR REPLACE FUNCTION validate_payload() RETURNS TRIGGER AS $$
BEGIN
IF NOT jsonb_path_exists(NEW.payload, '$.user_id ? (@.type() == "number")') THEN
RAISE EXCEPTION 'payload.user_id must exist and be a number';
END IF;
IF NOT jsonb_path_exists(NEW.payload, '$.tags ? (@.type() == "array" && size(@) > 0 && all(@[*].type() == "string"))') THEN
RAISE EXCEPTION 'payload.tags must be a non-empty array of strings';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
注意点:
-
jsonb_path_exists()在 PostgreSQL 12+ 才可用;旧版本只能退回到->+jsonb_typeof()组合 - 路径表达式里用
@.type()判断类型,不是typeof(@) -
size()和all()是路径函数,不能在普通 WHERE 条件里用
触发器性能和执行时机怎么选
用 BEFORE INSERT OR UPDATE 触发器,而不是 AFTER——后者没法阻止非法数据写入,只能补救,意义不大。
性能方面,jsonb_path_exists() 在简单路径下很快,但若路径含 **(递归下降)或复杂 all()/any() 表达式,可能显著拖慢写入。高频写入表建议:
- 只校验关键字段,避免对整个
payload做深度遍历 - 给常查的 JSON 键建
jsonb_path_opsGIN 索引(如CREATE INDEX ON events USING GIN ((payload #> '{user_id}'));),但索引不加速校验逻辑本身 - 如果校验规则极复杂(比如要调外部 HTTP 接口),别硬塞进触发器,改用应用层预检 + 数据库约束兜底
容易被忽略的边界情况
很多人忘了 NULL 和空对象的差异:
-
NULL值字段:触发器里NEW.payload IS NULL要单独判断,jsonb_path_exists(NULL, ...)返回NULL(不是false),直接参与IF NOT ...会跳过校验 - 空对象
'{}':它合法,但所有$.xxx路径都不存在,需显式允许或禁止 - 数字精度:
jsonb会把大整数转成 float(如9223372036854775807可能丢失精度),校验时用@.type() == "number"拦不住,得靠应用层控制输入
最麻烦的是嵌套结构里的可选字段——比如 $.meta?.version,PostgreSQL 14+ 才支持 ? 操作符,低版本得靠 jsonb_path_query_array() 或多层 COALESCE 套娃。


















