> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# pydantic_model_creator 完整用法
- URL: https://blog.vercanti.com/pydantic_model_creator-wan-zheng-yong-fa/
- Published: 2026-08-28T14:34:51.000Z
- Updated: 2026-08-28T14:57:30.000Z
- Description: 不同操作场景需要不同的 Schema，推荐将 Create、Update、Response 分离定义。 当模型包含外键或反向关系时，pydantic_model_creator 会自动将关联对象嵌套序列化，但必须先 prefetch_related。 computed 参数可以将模型上的 @property 方法暴露为 Pydantic Schema 的字段。 计算属性在 Pydantic Schema 中为只读字段（无法从外部赋值），类型根据 @property 的返回类型注解自动推断。 exclude_readonly=True 会自动排除所有只读字
- Author: yellowdog
- Tags: Tortoise-orm

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

---

## 一、函数签名与参数

```python
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\_now、auto\_now\_add、pk 等），用于创建 Schema |
| meta\_override       | type \| None       | None  | 覆盖自动生成的 Pydantic Meta 配置类                           |
| model\_config        | ConfigDict \| None | None  | 直接传入 Pydantic v2 的 ConfigDict，覆盖模型配置                |
| validators           | dict \| None       | None  | 额外的 Pydantic 字段验证器字典                                |

---

## 二、基础用法

### 2.1 模型定义

```python
# 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

```python
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 从模型实例转换

```python
# 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 分离定义。

```python
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 模型定义

```python
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

```python
# 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 包含作者信息

```python
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 的字段。

```python
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=True` 的 `DatetimeField`
- `auto_now_add=True` 的 `DatetimeField`
- `generated=True` 的字段

```python
UserCreate = pydantic_model_creator(
    User,
    name="UserCreate",
    exclude_readonly=True
)

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

```

---

## 七、与 FastAPI 集成完整示例

```python
# 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")

```

```python
# 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，以下是主要变化：

```python
# 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": "a@b.com"})

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

```

如果项目同时使用 Pydantic v1 和 v2（如通过兼容层），需要在配置中明确指定：

```python
from pydantic import ConfigDict

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

```

---

## 九、踩坑与注意事项

### 9.1 循环引用问题

**场景**：`User` 有多个 `Post`，`Post` 又有 `ForeignKey` 指向 `User`，两者互相引用。

**现象**：生成 Schema 时无限嵌套，或运行时 `RecursionError`。

**解决**：设置 `allow_cycles=True`，Tortoise 会在第一次循环时截断展开：

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

```

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

```python
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_related` 或 `select_related` 预取：

```python
# 错误：缺少 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的区别和用法](https://blog.vercanti.com/select%5Frelated-yu-prefetch%5Frelated-wan-quan-zhi-nan/)。

### 9.3 字段名与 Pydantic 保留字冲突

**场景**：模型字段名为 `schema`、`model_fields` 等 Pydantic 内部保留名称。

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

**解决**：修改模型字段名，或使用 `include` 参数只选择无冲突的字段。

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

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

**规范**：为每个 Schema 指定唯一的 `name` 参数。

```python
# 错误：相同 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 指定唯一 `name`**：`pydantic_model_creator` 有内部缓存，相同 `name` 的调用返回第一次创建的结果。为每个变体传入独立 `name`（如 `"UserPublic"`、`"UserCreate"`），避免参数不同却拿到同一个 Schema。

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

**需要关联数据时先 `prefetch_related`**：`pydantic_model_creator` 生成的 Schema 序列化关联字段时，若关联未加载会抛出 `NoValuesFetched`。在调用 `from_tortoise_orm` 之前务必预加载：

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

```

**输入 Schema 用 `include` 限制可写字段**：创建/更新接口只允许写入特定字段时，用 `include=("username", "email")` 明确白名单，防止客户端传入 `id`、`created_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 会在序列化时调用该属性。

---

## 参见

[查询操作完全指南](https://blog.vercanti.com/tortoise-orm-cha-xun-cao-zuo-wan-quan-zhi-nan/)  
[初始化与配置](https://blog.vercanti.com/tortoise-orm-chu-shi-hua-yu-pei-zhi/)  
[信号机制](https://blog.vercanti.com/xin-hao-ji-zhi/)