pydantic_model_creator 完整用法

不同操作场景需要不同的 Schema,推荐将 Create、Update、Response 分离定义。 当模型包含外键或反向关系时,pydantic_model_creator 会自动将关联对象嵌套序列化,但必须先 prefetch_related。 computed 参数可以将模型上的 @property 方法暴露为 Pydantic Schema 的字段。 计算属性在 Pydantic Schema 中为只读字段(无法从外部赋值),类型根据 @property 的返回类型注解自动推断。 exclude_readonly=True 会自动排除所有只读字

分享

官方文档:https://tortoise.github.io/contrib/pydantic.html
版本:tortoise-orm >= 0.20,pydantic >= 2.0
最后更新:2026-04-11


一、函数签名与参数

from tortoise.contrib.pydantic import pydantic_model_creator

UserSchema = pydantic_model_creator(User, name="UserSchema")

pydantic_model_creator 参数表

参数 类型 默认值 说明
cls Type[Model] 无,必填 Tortoise ORM 模型类
name str | None None 生成的 Pydantic 模型名称;None 则使用模型类名
exclude tuple[str, ...] () 需要排除的字段名元组,支持关联字段的嵌套路径(如 "posts.content"
include tuple[str, ...] () 仅包含的字段名元组;与 exclude 互斥
optional tuple[str, ...] () 标记为 Optional 的字段名元组,原本必填的字段变为可选
computed tuple[str, ...] () 额外包含的计算属性名元组(对应模型上的 @property
allow_cycles bool | None None 是否允许循环引用(允许时关联对象不无限展开),None 表示自动检测
sort_alphabetically bool | None None 是否按字母顺序排列字段,None 表示按模型定义顺序
exclude_readonly bool False 排除所有只读字段(auto_nowauto_now_addpk 等),用于创建 Schema
meta_override type | None None 覆盖自动生成的 Pydantic Meta 配置类
model_config ConfigDict | None None 直接传入 Pydantic v2 的 ConfigDict,覆盖模型配置
validators dict | None None 额外的 Pydantic 字段验证器字典

二、基础用法

2.1 模型定义

# models.py
from tortoise import fields
from tortoise.models import Model


class User(Model):
    id = fields.IntField(pk=True)
    username = fields.CharField(max_length=50, unique=True)
    email = fields.CharField(max_length=255)
    password_hash = fields.CharField(max_length=128)
    is_active = fields.BooleanField(default=True)
    created_at = fields.DatetimeField(auto_now_add=True)
    updated_at = fields.DatetimeField(auto_now=True)

    posts: fields.ReverseRelation["Post"]

    class Meta:
        table = "user"

2.2 生成基础 Schema

from tortoise.contrib.pydantic import pydantic_model_creator

# 生成包含所有字段的 Schema
UserSchema = pydantic_model_creator(User, name="UserSchema")

# 排除敏感字段
UserOut = pydantic_model_creator(
    User,
    name="UserOut",
    exclude=("password_hash",)
)

# 只包含指定字段
UserSummary = pydantic_model_creator(
    User,
    name="UserSummary",
    include=("id", "username", "email")
)

2.3 从模型实例转换

# from_tortoise_orm:从单个模型实例生成 Pydantic 模型
user = await User.get(id=1)
user_data = await UserOut.from_tortoise_orm(user)

# from_queryset:从 QuerySet 生成 Pydantic 模型列表
users = await UserOut.from_queryset(User.all())

# from_queryset_single:从返回单对象的 QuerySet 生成(不需要先 await)
user_data = await UserOut.from_queryset_single(User.get(id=1))

三、分层 Schema 策略

不同操作场景需要不同的 Schema,推荐将 Create、Update、Response 分离定义。

from tortoise.contrib.pydantic import pydantic_model_creator

# Response Schema:返回给客户端,排除敏感字段
UserOut = pydantic_model_creator(
    User,
    name="UserOut",
    exclude=("password_hash",)
)

# Create Schema:创建时不需要只读字段(id、created_at、updated_at)
UserCreate = pydantic_model_creator(
    User,
    name="UserCreate",
    exclude_readonly=True,      # 排除 id、auto_now、auto_now_add 字段
    exclude=("is_active",)      # 额外排除不需要用户填写的字段
)

# Update Schema:更新时所有字段都是可选的
UserUpdate = pydantic_model_creator(
    User,
    name="UserUpdate",
    exclude_readonly=True,
    exclude=("is_active",),
    optional=("username", "email", "password_hash")  # 所有字段变为可选
)

四、关联关系序列化

当模型包含外键或反向关系时,pydantic_model_creator 会自动将关联对象嵌套序列化,但必须先 prefetch_related

4.1 模型定义

class Post(Model):
    id = fields.IntField(pk=True)
    title = fields.CharField(max_length=200)
    content = fields.TextField()
    author: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
        "models.User", related_name="posts"
    )
    created_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "post"

4.2 生成带嵌套关联的 Schema

# User Schema 包含嵌套的 posts 列表
UserWithPosts = pydantic_model_creator(
    User,
    name="UserWithPosts",
    exclude=("password_hash",)
)

# 必须 prefetch_related,否则关联字段报错
user = await User.get(id=1).prefetch_related("posts")
data = await UserWithPosts.from_tortoise_orm(user)
# data.posts 为 PostSchema 列表

# 排除嵌套关联中的特定字段(使用点号路径)
UserWithPostTitles = pydantic_model_creator(
    User,
    name="UserWithPostTitles",
    exclude=("password_hash", "posts.content", "posts.created_at")
)

4.3 Post Schema 包含作者信息

PostWithAuthor = pydantic_model_creator(
    Post,
    name="PostWithAuthor",
    exclude=("author.password_hash",)
)

post = await Post.get(id=1).prefetch_related("author")
data = await PostWithAuthor.from_tortoise_orm(post)

五、computed 计算字段

computed 参数可以将模型上的 @property 方法暴露为 Pydantic Schema 的字段。

class User(Model):
    id = fields.IntField(pk=True)
    first_name = fields.CharField(max_length=50)
    last_name = fields.CharField(max_length=50)
    email = fields.CharField(max_length=255)

    @property
    def full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"

    @property
    def email_domain(self) -> str:
        return self.email.split("@")[-1]

    class Meta:
        table = "user"


# 包含计算属性
UserWithComputed = pydantic_model_creator(
    User,
    name="UserWithComputed",
    computed=("full_name", "email_domain")
)

user = await User.get(id=1)
data = await UserWithComputed.from_tortoise_orm(user)
print(data.full_name)      # "张 三"
print(data.email_domain)   # "example.com"

计算属性在 Pydantic Schema 中为只读字段(无法从外部赋值),类型根据 @property 的返回类型注解自动推断。


六、exclude_readonly 用于创建 Schema

exclude_readonly=True 会自动排除所有只读字段,包括:

  • 主键字段(pk=True
  • auto_now=TrueDatetimeField
  • auto_now_add=TrueDatetimeField
  • generated=True 的字段
UserCreate = pydantic_model_creator(
    User,
    name="UserCreate",
    exclude_readonly=True
)

# 等价于手动 exclude=("id", "created_at", "updated_at")
# 但 exclude_readonly 更健壮,不需要手动维护字段列表

七、与 FastAPI 集成完整示例

# schemas.py
from tortoise.contrib.pydantic import pydantic_model_creator
from myapp.models import User, Post

UserOut = pydantic_model_creator(User, name="UserOut", exclude=("password_hash",))
UserCreate = pydantic_model_creator(User, name="UserCreate", exclude_readonly=True)
UserUpdate = pydantic_model_creator(
    User,
    name="UserUpdate",
    exclude_readonly=True,
    optional=("username", "email", "password_hash", "is_active")
)
PostOut = pydantic_model_creator(Post, name="PostOut")
# routers/users.py
from fastapi import APIRouter, HTTPException
from passlib.hash import bcrypt
from myapp.models import User
from myapp.schemas import UserOut, UserCreate, UserUpdate

router = APIRouter(prefix="/users", tags=["users"])


@router.get("/{user_id}", response_model=UserOut)
async def get_user(user_id: int):
    user = await User.get_or_none(id=user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return await UserOut.from_tortoise_orm(user)


@router.get("/", response_model=list[UserOut])
async def list_users():
    return await UserOut.from_queryset(User.filter(is_active=True))


@router.post("/", response_model=UserOut, status_code=201)
async def create_user(payload: UserCreate):
    # UserCreate 是 Pydantic 模型,用 .model_dump() 获取字典
    data = payload.model_dump()
    data["password_hash"] = bcrypt.hash(data.pop("password_hash"))
    user = await User.create(**data)
    return await UserOut.from_tortoise_orm(user)


@router.patch("/{user_id}", response_model=UserOut)
async def update_user(user_id: int, payload: UserUpdate):
    user = await User.get_or_none(id=user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    # exclude_unset=True:只更新客户端实际传入的字段
    update_data = payload.model_dump(exclude_unset=True)
    await user.update_from_dict(update_data).save()
    return await UserOut.from_tortoise_orm(user)


@router.delete("/{user_id}", status_code=204)
async def delete_user(user_id: int):
    deleted = await User.filter(id=user_id).delete()
    if not deleted:
        raise HTTPException(status_code=404, detail="User not found")

八、Pydantic v2 兼容性注意事项

tortoise-orm 0.20+ 已全面支持 Pydantic v2,以下是主要变化:

# Pydantic v2 用 model_dump() 代替 dict()
data = user_schema.model_dump()
data = user_schema.model_dump(exclude_unset=True)  # 只获取设置过的字段

# Pydantic v2 用 model_validate() 代替 parse_obj()
user_schema = UserCreate.model_validate({"username": "alice", "email": "[email protected]"})

# JSON 序列化用 model_dump_json()
json_str = user_schema.model_dump_json()

如果项目同时使用 Pydantic v1 和 v2(如通过兼容层),需要在配置中明确指定:

from pydantic import ConfigDict

UserOut = pydantic_model_creator(
    User,
    name="UserOut",
    model_config=ConfigDict(from_attributes=True)  # Pydantic v2 必须设置
)

九、踩坑与注意事项

9.1 循环引用问题

场景User 有多个 PostPost 又有 ForeignKey 指向 User,两者互相引用。

现象:生成 Schema 时无限嵌套,或运行时 RecursionError

解决:设置 allow_cycles=True,Tortoise 会在第一次循环时截断展开:

UserWithPosts = pydantic_model_creator(
    User,
    name="UserWithPosts",
    allow_cycles=True,
    exclude=("password_hash",)
)

或者在一方的 Schema 中排除对方的关联字段:

PostOut = pydantic_model_creator(
    Post,
    name="PostOut",
    exclude=("author.posts",)  # 排除 author 中的 posts,避免循环
)

9.2 关联字段未 fetch 报错

现象await UserOut.from_tortoise_orm(user) 时报 OperationalError: You need to fetch ... first

原因:Schema 包含关联字段,但查询时没有预取关联数据。

解决:查询时用 prefetch_relatedselect_related 预取:

# 错误:缺少 prefetch_related
user = await User.get(id=1)
data = await UserWithPosts.from_tortoise_orm(user)  # 报错

# 正确
user = await User.get(id=1).prefetch_related("posts")
data = await UserWithPosts.from_tortoise_orm(user)  # 正常

# from_queryset 会自动处理预取,无需手动指定
users = await UserWithPosts.from_queryset(User.all())  # 自动 prefetch_related

预取关系详见 select_related 和 prefetch_related的区别和用法

9.3 字段名与 Pydantic 保留字冲突

场景:模型字段名为 schemamodel_fields 等 Pydantic 内部保留名称。

现象:生成的 Pydantic 模型行为异常或报错。

解决:修改模型字段名,或使用 include 参数只选择无冲突的字段。

9.4 name 参数不唯一导致 Schema 混用

pydantic_model_creator 内部会缓存相同 name 的 Schema。如果两处用相同 name 但不同参数调用,后者会复用前者的缓存,产生意料之外的结果。

规范:为每个 Schema 指定唯一的 name 参数。

# 错误:相同 name 不同 exclude
UserOut1 = pydantic_model_creator(User, name="User", exclude=("password_hash",))
UserOut2 = pydantic_model_creator(User, name="User", exclude=("email",))  # 实际是 UserOut1 的缓存

# 正确:使用不同名称
UserPublic = pydantic_model_creator(User, name="UserPublic", exclude=("password_hash",))
UserInternal = pydantic_model_creator(User, name="UserInternal", exclude=("email",))

最佳实践

每个 Schema 指定唯一 namepydantic_model_creator 有内部缓存,相同 name 的调用返回第一次创建的结果。为每个变体传入独立 name(如 "UserPublic""UserCreate"),避免参数不同却拿到同一个 Schema。

exclude 代替手写 Schema 字段:对于只需隐藏少数字段(如 password_hash)的场景,pydantic_model_creator(User, exclude=("password_hash",)) 比手写 Pydantic 模型更安全,新增 ORM 字段时 Schema 自动跟进,不会遗漏。

需要关联数据时先 prefetch_relatedpydantic_model_creator 生成的 Schema 序列化关联字段时,若关联未加载会抛出 NoValuesFetched。在调用 from_tortoise_orm 之前务必预加载:

user = await User.get(id=uid).prefetch_related('posts')
schema = await UserSchema.from_tortoise_orm(user)

输入 Schema 用 include 限制可写字段:创建/更新接口只允许写入特定字段时,用 include=("username", "email") 明确白名单,防止客户端传入 idcreated_at 等只读字段被意外写入。

异步批量序列化用 from_queryset:列表接口用 await UserSchema.from_queryset(User.all()) 而非循环调用 from_tortoise_orm,减少异步调度开销,且内部自动处理关联预加载。


常见陷阱

陷阱:相同 name 的 Schema 行为不符合预期

现象: pydantic_model_creator(User, name="User", exclude=("email",)) 返回的 Schema 仍然包含 email 字段。
原因: 之前已有 pydantic_model_creator(User, name="User") 的调用,缓存中存了无 exclude 的版本,后续同名调用直接返回缓存。
解决: 为每个变体使用唯一 name;若需强制重建(测试中),调用 User._pydantic_cache.clear() 清空缓存。

陷阱:序列化时 NoValuesFetched 异常

现象: 调用 await UserWithPostsSchema.from_tortoise_orm(user) 时抛出 NoValuesFetched: posts
原因: Schema 包含关联字段(posts),但 ORM 对象未预加载该关联。
解决: 查询时加上 prefetch_related('posts')user = await User.get(id=uid).prefetch_related('posts')

陷阱:computed 字段未出现在生成的 Schema 中

现象: 在 ORM 模型上用 @property 定义了计算属性,但 pydantic_model_creator 生成的 Schema 没有该字段。
原因: pydantic_model_creator 只识别 tortoise_orm 的字段描述符,Python 原生 @property 不在扫描范围内。
解决: 在模型的 PydanticMeta.computed 中声明计算属性名称:class PydanticMeta: computed = ("full_name",),Tortoise 会在序列化时调用该属性。


参见

查询操作完全指南
初始化与配置
信号机制

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog