
SQLModel 在启用数据库表映射(table=True)时,会绕过 Pydantic 的完整验证流程,导致 @field_validator 等装饰器完全不被调用,这是其底层设计限制所致。
sqlmodel 在启用数据库表映射(`table=true`)时,会绕过 pydantic 的完整验证流程,导致 `@field_validator` 等装饰器完全不被调用,这是其底层设计限制所致。
在 SQLModel 中,当模型声明为 table=True(即作为 ORM 映射表使用)时,其初始化逻辑会跳过 Pydantic v2 的标准验证管道——包括 @field_validator(mode="before")、@model_validator 等钩子均不会触发。这是因为 SQLModel 为兼容 SQLAlchemy 的实例化机制(如 Foo(data={"bar": 5}) 直接构造),默认采用 __init__ 的宽松赋值路径,而非走 model_validate() 入口。
这意味着你示例中的 serialize_dict 验证器永远不会执行,data 字段将原样接收传入的 dict,最终在插入数据库时因类型不匹配(期望 str,得到 dict)而抛出 SQL 绑定异常。
✅ 正确解决方案:分离输入模型与数据库模型
推荐遵循 FastAPI + SQLModel 最佳实践,严格区分「API 输入校验」和「数据库持久化」两个职责:
from pydantic import BaseModel, field_validator
import json
# ✅ 1. 专用输入模型(Pydantic v2 验证器完全生效)
class FooCreate(BaseModel):
data: str | dict # 允许原始 dict 输入
@field_validator("data", mode="before")
def serialize_dict(cls, value):
if isinstance(value, dict):
return json.dumps(value)
return value
# ✅ 2. 数据库模型(仅负责 ORM 映射,不加验证器)
class Foo(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
data: str # 存储为 JSON 字符串
# ✅ 3. API 路由中显式转换
@app.post("/foos")
def create_foo(
foo_in: FooCreate, # ← 使用带验证的输入模型
session: Session = Depends(get_session)
):
# 手动构建数据库模型实例
foo_db = Foo(data=foo_in.data) # 此时 foo_in.data 已是 str
session.add(foo_db)
session.commit()
session.refresh(foo_db)
return foo_db⚠️ 注意事项:
- 不要在
SQLModel子类(含table=True)上依赖@field_validator,这是已知且长期未修复的设计约束(见 sqlmodel#453); - 若需运行时数据转换,务必通过中间 Pydantic 模型完成预处理;
- 对于复杂场景(如嵌套对象序列化/反序列化),可配合
@computed_field或自定义__init__(但需谨慎避免破坏 SQLAlchemy 兼容性); - 升级至较新版本(如 SQLModel ≥ 0.0.29 + Pydantic ≥ 2.8)亦无法改变该行为——它根植于 SQLModel 的双继承架构取舍。
总之,这不是 bug,而是 SQLModel 为平衡 ORM 功能与 Pydantic 验证所作出的明确设计选择。坚持“输入模型校验、DB 模型纯净”的分层模式,才是稳健可靠的工程实践。

















