Skip to content

第 6 章 嵌套模型与错误定位 ​

前面几章,我们一直在描述文章自己的字段。文章详情里还会带作者信息和评论列表,而每条评论也有自己的字段。都塞进 ArticleCreate 一个模型里,字段很快就会混在一起;拆成几个模型,再组合起来会清楚得多。

之前的文章模型大致是这样:

python
from pydantic import BaseModel, Field


class ArticleCreate(BaseModel):
    title: str = Field(min_length=2, max_length=50)
    content: str = Field(min_length=10)
    views: int = Field(default=0, ge=0, le=1_000_000)
    is_published: bool = Field(default=False)
    tags: list[str] = Field(default_factory=list, max_length=5)
    summary: str | None = Field(default=None, max_length=100)

现在的输入多了作者和评论:

python
data = {
    "title": "Pydantic 嵌套模型入门",
    "content": "这是一篇介绍嵌套模型的文章内容",
    "author": {
        "username": "xiaoming",
        "email": "xiaoming@example.com"
    },
    "comments": [
        {
            "username": "alice",
            "content": "这篇文章很清晰",
            "likes": 3
        },
        {
            "username": "bob",
            "content": "期待下一章",
            "likes": 5
        }
    ]
}

author 是一组固定结构的数据,comments 是一组评论数据。我们要把它们拆成模型,再让文章模型组合这些部分;如果某个字段出错,还要能从错误位置看出是作者还是哪条评论出了问题。

本章只讲数据结构的拆分和组合,不涉及数据库关系、ORM、接口层或权限系统。

本章学习目标 ​

文章里有作者,也有一条条评论。你会把作者信息和评论各自定义成模型,再把模型放进文章字段,用 list[CommentCreate] 表示评论列表。校验出错时,试着沿着字段名和列表下标找到具体是哪一层出了问题。

嵌套模型构建 ​

文章详情可以看成几个模型组合在一起:

text
ArticleDetail
├── title
├── content
├── author -> UserInfo
└── comments -> list[CommentCreate]

ArticleDetail 嵌套数据模型拓扑结构

这张图描述的是数据结构,不是数据库表之间的关系。

每个完整代码块都可以复制到本地 .code/main.py。切换示例时覆盖这个文件,然后在 .code 目录运行 uv run python main.py。

作者信息模型 ​

一个模型的字段也可以使用另一个 Pydantic 模型作为类型,这就是嵌套模型。比如:

python
class UserInfo(BaseModel):
    username: str
    email: str


class ArticleDetail(BaseModel):
    title: str
    content: str
    author: UserInfo

author: UserInfo 表示作者字段要符合 UserInfo 的结构。把字典交给 ArticleDetail 校验后,article.author 会是 UserInfo 模型对象,可以这样访问:

python
article.author.username

先只处理作者信息,运行这个最小示例:

py
from pydantic import BaseModel, Field


class UserInfo(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    email: str


class ArticleDetail(BaseModel):
    title: str = Field(min_length=2, max_length=50)
    content: str = Field(min_length=10)
    author: UserInfo


data = {
    "title": "Pydantic 嵌套模型入门",
    "content": "这是一篇介绍 Pydantic 嵌套模型的文章",
    "author": {
        "username": "xiaoming",
        "email": "xiaoming@example.com",
    },
}

article = ArticleDetail.model_validate(data)

print(article.title)
print(article.author)
print(article.author.username)
print(article.author.email)

运行:

bash
uv run python main.py

可能看到类似输出:

text
Pydantic 嵌套模型入门
username='xiaoming' email='xiaoming@example.com'
xiaoming
xiaoming@example.com

如果不拆模型,就得把作者和评论字段直接塞进文章类:

python
author_username: str
author_email: str
comment_1_username: str
comment_1_content: str
comment_1_likes: int

这样作者字段和文章字段混在一起,评论数量也不好表示,更没法复用同一套评论结构。拆开后,作者由 UserInfo 描述,评论由 CommentCreate 描述,文章详情由 ArticleDetail 组合它们。

评论模型列表 ​

单个嵌套字段之外,列表中的每一项也可以是一个模型:

python
comments: list[CommentCreate]

这表示 comments 是评论列表,而且每条评论都要符合 CommentCreate 的结构。校验成功后,可以这样访问:

python
article.comments[0].content
article.comments[1].likes

把评论模型也加入文章详情:

py
from pydantic import BaseModel, Field


class UserInfo(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    email: str


class CommentCreate(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    content: str = Field(min_length=2, max_length=200)
    likes: int = Field(default=0, ge=0)


class ArticleDetail(BaseModel):
    title: str = Field(min_length=2, max_length=50)
    content: str = Field(min_length=10)
    author: UserInfo
    comments: list[CommentCreate] = Field(default_factory=list)


data = {
    "title": "Pydantic 嵌套模型入门",
    "content": "这是一篇介绍 Pydantic 嵌套模型的文章",
    "author": {
        "username": "xiaoming",
        "email": "xiaoming@example.com",
    },
    "comments": [
        {
            "username": "alice",
            "content": "这篇文章很清晰",
            "likes": 3,
        },
        {
            "username": "bob",
            "content": "期待下一章",
        },
    ],
}

article = ArticleDetail.model_validate(data)

print(article.author.username)
print(article.comments)
print(article.comments[0].content)
print(article.comments[1].likes)

运行:

bash
uv run python main.py

可能看到类似输出:

text
xiaoming
[CommentCreate(username='alice', content='这篇文章很清晰', likes=3), CommentCreate(username='bob', content='期待下一章', likes=0)]
这篇文章很清晰
0

comments: list[CommentCreate] = Field(default_factory=list) 还设置了默认空列表。示例里的第二条评论没有传 likes,所以用 CommentCreate 上的默认值 0:

python
likes: int = Field(default=0, ge=0)

校验成功后,article.comments[1].likes 的值就是 0。

嵌套模型递归校验流转流水线

ArticleDetail 负责整体结构,UserInfo 描述作者,CommentCreate 描述每条评论。Pydantic 会逐层校验,再生成嵌套模型对象。

访问校验后的嵌套对象 ​

嵌套字典和列表通过校验后,会变成相应的模型对象。下面这张图展示可以怎样访问它们:

正向点号属性链访问 vs 逆向 loc 错误路径回溯

前面的结构图回答“模型里有哪些字段”,这张图回答“校验通过后怎么取值”。常见访问方式是:

python
article.author.username
article.comments[0].content
article.comments[1].likes

嵌套错误定位 ​

嵌套校验错误 ​

模型字段声明为 author: UserInfo 和 comments: list[CommentCreate] 后,Pydantic 不只检查 author 是不是字典、comments 是不是列表,还会继续检查内部字段,例如 author.username、comments[0].content 和 comments[0].likes。

外层模型管整体结构,内层模型管各自的数据结构;校验时它们会一起工作。下面的错误数据故意包含三处问题:作者用户名太短、第二条评论内容太短、第二条评论的点赞数是负数。

py
from pydantic import BaseModel, Field, ValidationError


class UserInfo(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    email: str


class CommentCreate(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    content: str = Field(min_length=2, max_length=200)
    likes: int = Field(default=0, ge=0)


class ArticleDetail(BaseModel):
    title: str = Field(min_length=2, max_length=50)
    content: str = Field(min_length=10)
    author: UserInfo
    comments: list[CommentCreate] = Field(default_factory=list)


bad_data = {
    "title": "Pydantic 嵌套模型入门",
    "content": "这是一篇介绍 Pydantic 嵌套模型的文章",
    "author": {
        "username": "x",
        "email": "xiaoming@example.com",
    },
    "comments": [
        {
            "username": "alice",
            "content": "这篇文章很清晰",
            "likes": 3,
        },
        {
            "username": "bob",
            "content": "好",
            "likes": -1,
        },
    ],
}

try:
    article = ArticleDetail.model_validate(bad_data)
    print(article)
except ValidationError as e:
    print("普通错误信息:")
    print(e)

    print("结构化错误信息:")
    for error in e.errors():
        print(error)

运行:

bash
uv run python main.py

错误输出可能还包含错误类型和输入值,先看字段路径和原因:

text
普通错误信息:
3 validation errors for ArticleDetail
author.username
  String should have at least 2 characters
comments.1.content
  String should have at least 2 characters
comments.1.likes
  Input should be greater than or equal to 0

author.username 指向作者的用户名;comments.1.content 和 comments.1.likes 指向列表索引 1,也就是第二条评论里的字段。

在结构化错误中,位置通常是一个 loc 元组:

python
('author', 'username')

对应原始输入里的:

python
data["author"]["username"]

列表也一样:

python
('comments', 1, 'likes')

对应:

python
data["comments"][1]["likes"]

读路径时,comments 是列表字段,1 是第二项的下标,likes 是那条评论的字段名。遇到嵌套错误,先看 loc,再沿着路径回到原始数据。

提取错误位置 ​

如果完整错误字典太长,可以从 e.errors() 中只取位置、说明和输入值:

py
from pydantic import BaseModel, Field, ValidationError


class UserInfo(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    email: str


class CommentCreate(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    content: str = Field(min_length=2, max_length=200)
    likes: int = Field(default=0, ge=0)


class ArticleDetail(BaseModel):
    title: str = Field(min_length=2, max_length=50)
    content: str = Field(min_length=10)
    author: UserInfo
    comments: list[CommentCreate] = Field(default_factory=list)


bad_data = {
    "title": "Pydantic 嵌套模型入门",
    "content": "这是一篇介绍 Pydantic 嵌套模型的文章",
    "author": {
        "username": "x",
        "email": "xiaoming@example.com",
    },
    "comments": [
        {
            "username": "alice",
            "content": "这篇文章很清晰",
            "likes": 3,
        },
        {
            "username": "bob",
            "content": "好",
            "likes": -1,
        },
    ],
}

try:
    article = ArticleDetail.model_validate(bad_data)
    print(article)
except ValidationError as e:
    for error in e.errors():
        location = " -> ".join(str(item) for item in error["loc"])
        print("错误位置:", location)
        print("错误说明:", error["msg"])
        print("输入值:", error["input"])
        print("-" * 30)

运行:

bash
uv run python main.py

位置会依次输出为:

text
错误位置: author -> username
错误位置: comments -> 1 -> content
错误位置: comments -> 1 -> likes

每个位置后面还会跟着错误说明和原始输入,方便对照数据排查。

模型拆分与综合案例 ​

判断哪些字段适合拆成模型 ​

简单字段继续留在当前模型就可以:

python
title: str
views: int
summary: str | None

如果字段本身包含一组固定的子字段,就可以考虑把它拆成模型。比如作者信息:

text
author: {
    "username": "...",
    "email": "..."
}

可以定义为:

python
class UserInfo(BaseModel):
    username: str
    email: str

评论列表同理:

text
comments: [
    {"username": "...", "content": "...", "likes": 0},
    {"username": "...", "content": "...", "likes": 3}
]

每项的结构固定,可以用一个评论模型反复描述:

python
class CommentCreate(BaseModel):
    username: str
    content: str
    likes: int

下面这张图可以用来判断:

复杂数据字段拆分为独立模型的架构决策树

一组结构化数据可以拆成一个模型;多个同结构对象可以用模型列表表示。

文章详情嵌套校验 ​

最后运行完整案例:它会先校验并访问正确文章,再提交含错误用户名和负数点赞数的数据,观察嵌套错误路径。

py
from pydantic import BaseModel, Field, ValidationError


class UserInfo(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    email: str


class CommentCreate(BaseModel):
    username: str = Field(min_length=2, max_length=20)
    content: str = Field(min_length=2, max_length=200)
    likes: int = Field(default=0, ge=0)


class ArticleDetail(BaseModel):
    title: str = Field(min_length=2, max_length=50)
    content: str = Field(min_length=10)
    author: UserInfo
    comments: list[CommentCreate] = Field(default_factory=list)


data = {
    "title": "Pydantic 嵌套模型入门",
    "content": "这是一篇介绍 Pydantic 嵌套模型的文章",
    "author": {
        "username": "xiaoming",
        "email": "xiaoming@example.com",
    },
    "comments": [
        {
            "username": "alice",
            "content": "这篇文章很清晰",
            "likes": 3,
        },
        {
            "username": "bob",
            "content": "期待下一章",
            "likes": 5,
        },
    ],
}

article = ArticleDetail.model_validate(data)

print("文章标题:", article.title)
print("作者用户名:", article.author.username)
print("作者邮箱:", article.author.email)
print("第一条评论内容:", article.comments[0].content)
print("第二条评论点赞数:", article.comments[1].likes)


bad_data = {
    "title": "Pydantic 嵌套模型入门",
    "content": "这是一篇介绍 Pydantic 嵌套模型的文章",
    "author": {
        "username": "x",
        "email": "xiaoming@example.com",
    },
    "comments": [
        {
            "username": "alice",
            "content": "这篇文章很清晰",
            "likes": 3,
        },
        {
            "username": "bob",
            "content": "好",
            "likes": -1,
        },
    ],
}

try:
    bad_article = ArticleDetail.model_validate(bad_data)
    print(bad_article)
except ValidationError as e:
    print("错误数据校验失败")
    print(e)

    for error in e.errors():
        location = " -> ".join(str(item) for item in error["loc"])
        print("错误位置:", location)
        print("错误说明:", error["msg"])
        print("输入值:", error["input"])
        print("-" * 30)

运行:

bash
uv run python main.py

成功时,作者和评论字段都可以按模型属性访问;错误数据会在 author.username 和 comments[1].likes 等位置报错。脚本还会逐条打印每个错误的位置、说明和输入值。

本章小结 ​

像作者、评论这样有固定子字段的数据,单独建模后再组合更清楚:author: UserInfo 放作者,list[CommentCreate] 放评论。校验通过后,它们是可访问的模型对象;失败时,顺着 loc 中的字段名和下标往里找。