首页
/ FastAPI 嵌套模型实战指南:用 Pydantic 构建任意深度的请求体结构

FastAPI 嵌套模型实战指南:用 Pydantic 构建任意深度的请求体结构

2026-09-06 23:12:13作者:范靓好Udolf

本篇技术指南基于 FastAPI 官方教程中“Body – 嵌套模型(verschachtelte Modelle)”一章展开,系统讲解如何利用 Pydantic 在 FastAPI 中定义、校验和文档化任意深度的嵌套 JSON 请求体——包括带类型参数的列表、去重集合、子模型、list[子模型]、纯列表请求体以及任意键类型的 dict 请求体。读完后,你将能够编写出“声明即校验、声明即文档”的复杂请求体处理代码,并理解 FastAPI 背后的参数解析与测试验证机制。

为什么嵌套模型是 FastAPI 的核心能力

借助 Pydantic,FastAPI 允许你定义任意深度的嵌套模型,并对它们进行校验、类型转换与自动文档生成。官方教程中所有示例代码都位于 docs_src/body_nested_models/ 目录,对应的自动化测试则位于 tests/test_tutorial/test_body_nested_models/ 目录(包含 test_tutorial001_tutorial002_tutorial003.pytest_tutorial004.pytest_tutorial009.py 等文件),可运行测试验证每一段示例代码的真实行为。

一、列表作为字段:从裸 list 起步

最简单的场景是把某个属性声明为 Python 的 list 类型。以 tutorial001_py310.py 为例:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

这里 tags: list = [] 表示 tags 是一个列表,但没有约束列表内部元素的类型——它可以是任意 JSON 值。同时注意 tags 的默认值是空列表,因此它是可选字段。端点函数签名 update_item(item_id: int, item: Item) 中,item 的注解是 Pydantic 模型,FastAPI 会自动将其解析为请求体(而非路径或查询参数)。

二、带类型参数的列表:list[str]

Python 提供了声明“内部类型”(即类型参数)的标准语法,适用于 listdicttuple 等容器类型——把内部类型放在方括号 [] 中:

my_list: list[str]

这是纯标准 Python 类型声明语法,对模型属性同样适用。在 tutorial002_py310.py 中,我们把 tags 收紧为“字符串列表”:

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []

这一行改动带来的差异:

声明 语义 非法输入示例
tags: list 元素类型不受限 无法拒绝
tags: list[str] 每个元素必须是字符串(或可转为字符串) {"tags": [123.4, null]} 会触发 422 校验错误

对应的测试 test_tutorial001_tutorial002_tutorial003.py 会用 TestClient 实际发送包含非法元素类型的请求体,验证 FastAPI 返回 422。

三、set 类型:自动去重

标签(tags)通常不应重复,Python 专门用于唯一元素集合的数据类型是 set。在 tutorial003_py310.py 中:

tags: set[str] = set()

这带来三个关键行为:

  1. 入站去重:即使客户端在请求中发送了重复数据(如 {"tags": ["rock", "rock", "metal"]}),FastAPI 也会将其转换为唯一元素的集合;
  2. 出站稳定输出:无论源头是否有重复,序列化输出时始终是去重后的集合;
  3. 文档同步:OpenAPI 文档中会相应标注为 uniqueItems 的数组类型,自动文档与实际行为一致。

四、嵌套模型:用子模型描述深层 JSON 对象

Pydantic 模型的每个属性都有类型,而这个类型本身可以是另一个 Pydantic 模型。这样你就能声明带特定属性名、类型和校验规则的深层嵌套 JSON “对象”,且嵌套深度不受限制。

4.1 定义子模型

tutorial004_py310.py 中,先定义 Image 模型:

class Image(BaseModel):
    url: str
    name: str

4.2 把子模型用作属性类型

然后把它作为 Item 中某个属性的类型:

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None

这样 FastAPI 就期望接收类似如下的请求体:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}

仅凭这一处类型声明,FastAPI 就免费提供:

  • 编辑器支持(代码补全等),即便针对嵌套模型内部字段;
  • 数据转换(type coercion,例如把数字字符串转为数值);
  • 数据校验(字段缺失、类型不符返回 422);
  • 自动文档(OpenAPI 中自动生成 Image 组件并引用)。

对应测试 test_tutorial004.py 验证了嵌套 image 字段的解析结果,以及缺失 url 等字段时返回的校验错误结构。

五、特殊类型与校验:HttpUrl 的实战应用

除了 strintfloat 等基础类型,还可以使用继承自 str 的更复杂的简单类型。要了解全部可选项,可查阅 Pydantic 官方的类型概览(tutorial005_py310.py 中导入的 HttpUrl 即属此类)。

例如 Image 模型中有一个 url 字段,我们可声明它必须是 Pydantic 的 HttpUrl 实例,而不是普通的 str

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str

(完整代码见 tutorial005_py310.py。)

效果有二:

  1. 校验:Pydantic 会测试该字符串是否为合法 URL,非法 URL(如 http:// 或空串)会直接返回 422 校验错误,测试 test_tutorial005.py 覆盖了这一场景;
  2. 文档:该字段会以 URL 格式(format: uri 等)记录在 JSON Schema / OpenAPI 中,Swagger UI 会给出更精确的提示。

六、属性类型为“子模型列表”:list[Image]

Pydantic 模型还可以作为 listset 等容器类型的内部类型使用。在 tutorial006_py310.py 中,Itemimages 字段被声明为:

images: list[Image] | None = None

此时 FastAPI 期望(并完成转换、校验、文档化)的 JSON 请求体形如:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": [
        "rock",
        "metal",
        "bar"
    ],
    "images": [
        {
            "url": "http://example.com/baz.jpg",
            "name": "The Foo live"
        },
        {
            "url": "http://example.com/dave.jpg",
            "name": "The Baz"
        }
    ]
}

注意 images 键现在是一个图像对象数组,列表中每个元素都会被逐一按 Image 模型校验。测试 test_tutorial006.py 验证了多张图片的解析及单张图片字段缺失时的错误响应。

七、深度嵌套模型:Offerlist[Item]list[Image]

嵌套深度可以任意增加。tutorial007_py310.py 展示了三层嵌套结构:

class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


class Offer(BaseModel):
    name: str
    description: str | None = None
    price: float
    items: list[Item]


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer

注意整个结构链:Offer 持有一个 Item 列表,而每个 Item 又可选地持有 Image 列表。OpenAPI 文档中这三个模型都会生成独立的 schema 组件并相互 $ref 引用。测试 test_tutorial007.py 会发送包含多个商品、每个商品带多张图片的完整深度嵌套请求体,并校验回显结果。

八、纯列表请求体:顶层就是 JSON 数组

通常请求体顶层是一个 JSON 对象,但如果你期望的最外层值就是一个 JSON array(Python 的 list),同样可以直接在函数参数上声明类型,语法与 Pydantic 模型字段完全一致:

images: list[Image]

tutorial008_py310.py 所示:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images

客户端此时发送的不是对象,而是数组本身:

[
    {"url": "http://example.com/baz.jpg", "name": "The Foo live"},
    {"url": "http://example.com/dave.jpg", "name": "The Baz"}
]

从源码结构看,FastAPI 在 fastapi/dependencies/ 模块中通过分析函数参数注解来判断该参数是路径参数、查询参数还是请求体——当注解是 Pydantic 模型或序列容器(list 等)时,整个参数会被视为 body 来源。测试 test_tutorial008.py 验证了纯数组请求体的正常解析,以及发送非数组数据时的校验失败行为。

九、编辑器支持无处不在

由于每一层都是强类型声明,你在编辑器中能获得完整的自动补全支持,包括列表内部的元素

FastAPI 嵌套模型在编辑器中的自动补全效果,展示 list[Image] 字段内部元素的代码提示

(图片源自官方教程:对 list[Image] 类型字段的编辑器代码补全。)

如果直接操作 dict 是拿不到这种编辑器支持的。但你也无需为此操心:传入的 dict 会被自动转换(解析)为模型实例,输出时也会自动序列化为 JSON。

十、任意 dict 请求体:键值各自强类型

有时你事先并不知道合法的字段名(这正是 Pydantic 模型所要求的预先声明),却仍然希望接收任意键。这时可以直接把请求体声明为 dict,并分别为键和值指定类型。

另一个典型场景是需要非字符串类型的键,比如 int 键。在 tutorial009_py310.py 中:

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: dict[int, float]):
    return weights

这表示:只要传入的是 int 键、float 值的任意 dict 即可接受。例如发送:

{
    "index1": 10.5,
    "index2": 20.25
}

这里有一个重要的细节:JSON 规范只允许字符串作为键。但 Pydantic 具备自动数据转换能力——API 客户端实际只能发送字符串键,Pydantic 会自动把它们转换并校验为 int(前提是字符串内容为纯整数),最终端点函数里拿到的 weights 确实具有 int 键和 float 值。测试 test_tutorial009.py 验证了“字符串数字键被成功转换”和“非数字键返回 422”两种情况。

十一、各示例的输入能力速查表

示例文件 关键字段声明 接受的请求体形态 关键行为
tutorial001_py310.py tags: list = [] 任意元素列表 元素类型不校验
tutorial002_py310.py tags: list[str] = [] 字符串数组 非字符串元素返回 422
tutorial003_py310.py tags: set[str] = set() 字符串数组(可重复) 入站去重,文档标注 uniqueItems
tutorial004_py310.py image: Image | None 内嵌 image 对象 Image 模型深度校验
tutorial005_py310.py url: HttpUrl url 必须为合法 URL 非法 URL 返回 422,文档标注 uri 格式
tutorial006_py310.py images: list[Image] | None 对象数组 逐元素校验
tutorial007_py310.py items: list[Item] 三层嵌套对象 OfferItemImage 全链路校验
tutorial008_py310.py 参数 images: list[Image] 顶层即为 JSON 数组 每个元素按 Image 校验
tutorial009_py310.py 参数 weights: dict[int, float] 任意键值对 JSON 字符串键自动转换为 int

总结

FastAPI 中,你获得了 Pydantic 模型的全部表达能力——任意深度的嵌套、容器类型参数化、特殊校验类型、强类型 dict 键——而代码本身保持简洁、短小、优雅。并且你始终享有这些能力:

  • 编辑器支持:代码补全无处不在,深入嵌套结构内部;
  • 数据转换:又称解析(parsing)/序列化(serialization),如字符串数字键转 int
  • 数据校验:结构不符、类型不符自动返回 422 及详细错误位置;
  • Schema 文档:每个嵌套模型自动生成为 OpenAPI 组件;
  • 自动文档:Swagger UI 中可直接试错嵌套结构,并得到逐字段的错误提示。

所有上述行为均有对应的自动化测试(tests/test_tutorial/test_body_nested_models/)持续验证,示例代码可复制自 docs_src/body_nested_models/ 直接在本地项目中运行。

登录后查看全文
热门项目推荐
相关项目推荐