首页
/ FastAPI 嵌套模型请求体全解:从 list/set 字段到任意深度的 Pydantic 模型

FastAPI 嵌套模型请求体全解:从 list/set 字段到任意深度的 Pydantic 模型

2026-09-06 18:12:33作者:侯霆垣

在开发 REST API 时,真实业务请求体很少是扁平的——订单里包含商品列表、商品里又挂图片对象、图片又有 URL 字段。FastAPI 借助 Pydantic,允许你定义、校验、文档化并处理任意深度嵌套的请求体模型,从简单的 list / set 字段,到「模型嵌套列表、列表再嵌套模型」的多层结构,再到键类型不受限的任意 dict,全部用标准 Python 类型注解即可完成。

本篇以仓库文档 docs/en/docs/tutorial/body-nested-models.md 为骨架,结合 docs_src/body_nested_models/ 下 9 个可直接运行的示例源文件,带你逐层掌握嵌套模型字段声明、list[str] / set[str] 类型参数、子模型组合、HttpUrl 等特殊字符串类型、纯列表请求体与 dict[int, float] 任意字典请求体。学完后你不仅能写对代码,还能理解编辑器补全、数据转换、校验与 OpenAPI 自动文档背后 Pydantic 起作用的机制。

为什么 FastAPI 能“免费”处理嵌套 JSON

FastAPI 的路由层会读取请求体并将其交给 Pydantic 完成解析(parsing)与校验(validation),返回时再由响应层完成序列化(serialization)。因此,只要你在 Pydantic 模型中表达清楚类型结构,嵌套的 JSON 对象与数组就能自动被递归地转换、校验和文档化。整条链条可以用仓库里的最小示例 docs_src/body_nested_models/tutorial001_py310.py 作为起点来体会——一个模型字段是否声明“内部类型”,直接决定了数据校验的严格程度,这正是本教程第一节讨论的差异。

普通 List 字段

在 Pydantic 模型中,字段可以是任意 Python 类型,包括 list。定义一个 Item 模型并声明 tagslist

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

示例源码见 docs_src/body_nested_models/tutorial001_py310.py。注意:这样声明后 tags 确实会被当作一个 list 来解析,但它没有声明列表元素的类型,因此校验器不会对 list 内单个元素做任何约束——数字、字符串、对象混在一起都能通过。真正的约束来自下面一节介绍的“类型参数”。

带类型参数的 List 字段

Python 的标准类型语法允许为容器类型声明内部元素类型,也就是“类型参数”(type parameters)。写法是在方括号 [ ] 中传入内部类型:

my_list: list[str]

这完全是 Python 标准类型声明语法,直接把它用于模型属性即可。把上面的 tags 明确为「字符串列表」:

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

示例源码见 docs_src/body_nested_models/tutorial002_py310.py。此后收到请求时,FastAPI / Pydantic 会逐个检查 tags 中的元素,每个都必须是字符串,否则返回 422 校验错误;生成 OpenAPI/JSON Schema 时,该字段也会被标记为 array + items.type = string,在交互式文档中体现为元素类型明确的数组输入框。

Set 类型:自动去重

实际业务里标签通常不应重复,每个 tag 应当是唯一字符串。Python 恰好有保存唯一元素的专用类型——set。声明 tags 为字符串集合:

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

示例源码见 docs_src/body_nested_models/tutorial003_py310.py。使用 set 后你会获得三个层面的效果:

  1. 入站转换:即使请求中带重复数据(例如 ["rock", "metal", "rock"]),也会被转换成唯一元素集合;
  2. 出站序列化:无论数据源内部是否有重复,输出响应时都以集合(唯一元素)形式返回;
  3. 文档标注:OpenAPI / JSON Schema 与交互式文档会相应地标记该字段类型。

嵌套模型(Nested Models)

Pydantic 模型中每个属性都有自己的类型,而这个类型本身又可以是另一个 Pydantic 模型。这样一来,你就能声明包含具体属性名、类型与校验规则的深层 JSON“对象”,且可以任意多层嵌套。

定义子模型

例如先定义一个 Image 模型:

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

把子模型用作属性类型

然后把它作为另一个模型属性的类型(对应源码 docs_src/body_nested_models/tutorial004_py310.py):

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


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


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


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

此时 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"
    }
}

注意 image: Image | None = None 使图片成为可选字段:客户端可以传、也可以不传 image。只要做了这一处声明,FastAPI 就自动为你提供:

  • 编辑器支持:即使对嵌套模型也能自动补全、类型检查;
  • 数据转换:把请求 JSON 递归转换为对应的 Pydantic 模型对象;
  • 数据校验:内层字段缺失、类型不符、多余校验规则不满足时返回详细错误;
  • 自动文档:OpenAPI 会为嵌套对象递归生成 $ref 引用结构,交互式文档中展示完整的嵌套 JSON 层级。

特殊类型与校验:以 HttpUrl 为例

除了 strintfloat 等常规单一类型,Pydantic 还提供大量从 str 派生、带更强约束的“特殊字符串类型”。完整清单可查阅 Pydantic 官方类型总览(本仓库 README 与文档所依托的 Pydantic 生态能力);下一章会有更多示例。

一个典型例子就是 URL 校验:Image 模型里的 url 字段不声明为普通 str,而是声明为 Pydantic 的 HttpUrl(源码 docs_src/body_nested_models/tutorial005_py310.py):

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


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()
    image: Image | None = None


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

效果是:传入的字符串会被校验是否为合法 URL,同时在 JSON Schema / OpenAPI 中被标记为 format: uri。如果客户端传了 "example.com"(缺协议)或随意乱写,请求将被 422 校验错误拦截。

子模型组成的 List 字段

Pydantic 模型同样可以作为 listset 等容器的内部元素类型(源码 docs_src/body_nested_models/tutorial006_py310.py):

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

这时 FastAPI 会期望(并转换、校验、文档化)如下结构的 JSON body——images 是一个「图片对象数组」:

{
    "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 模型校验(此处 url 还会被校验为合法 HttpUrl)。

任意深度的嵌套模型

嵌套没有层级上限。把 Item(内含可选 list[Image])再放进 Offeritems 列表中,就构成了三层结构(源码 docs_src/body_nested_models/tutorial007_py310.py):

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


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 列表。端点 POST /offers/ 直接以整个 Offer 为请求体,返回值把模型交给 FastAPI 自动序列化。这种“列表套模型、模型再套可选列表”的写法覆盖了绝大多数电商、内容管理等业务场景,且校验会递归穿透每一层——任何一层的 URL 非法、缺少必填字段都会在入口被拦下。

纯 List 形式的请求体

以上请求体的顶层都是 JSON 对象。如果你的接口期望的顶层值本身就是一个 JSON 数组(Python list),直接在函数参数上声明即可,写法与在模型字段中完全一致:

images: list[Image]

完整示例(源码 docs_src/body_nested_models/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

此时客户端 POST 的 body 顶层就是 Image 数组,例如:

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

FastAPI 会把整个顶层数组解析为 list[Image] 后传入 images 参数,函数内可直接遍历访问每个元素的类型化字段。

处处可用的编辑器支持

嵌套结构的另一大红利是编辑器支持无处不在——包括列表内部元素的补全与类型推断。

嵌套模型中列表内元素的编辑器补全提示(来源:docs/en/docs/img/tutorial/body-nested-models/image01.png)

图片展示的正是 docs_src/body_nested_models/tutorial008_py310.py 这类 list[Image] 场景:当你在代码里遍历 images 并访问 image.url 时,编辑器能基于 Image 模型直接给出字段补全与类型标注。这种体验在使用裸 dict 时完全无法获得——dict 对编辑器来说是“不透明”的键值对。不过你也不必担心转换负担:入站 dict 会被自动转成模型对象,出站对象也会被自动序列化为 JSON。

任意键类型的 dict 请求体

还有一种场景是:你不希望(或无法)预先枚举请求体的字段名。此时可以声明一个键和值各有类型的 dict,不必像 Pydantic 模型那样预先知道所有合法属性名。例如想让 body 接收任意 int 键对应 float 值的字典(源码 docs_src/body_nested_models/tutorial009_py310.py):

from fastapi import FastAPI

app = FastAPI()


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

一个典型用途是接收类似「下标 → 权重」的稀疏映射:接口不需要预知键名集合,例如客户端发送:

{
    "1": 2.5,
    "3": 8.8,
    "7": 4.2
}

这里有个需要牢记的细节:JSON 规范只允许字符串作为对象键。但由于 Pydantic 具备自动数据转换能力,只要客户端发送的键字符串里是纯整数(如 "1""3"),Pydantic 就会把它们转换并校验为 int。于是你在 weights 参数中拿到的字典,真实键类型是 int、值类型是 float,可以直接用于数值索引计算,无需再做一次字符串到整数的转换。传入顺序无关紧要,因此这种 dict 形式也天然适合扩展性要求高的动态配置接口。

小结

通过上面的层层递进可以看到,FastAPI 把 Pydantic 模型的能力放到了最大,同时让代码保持简单、简短、优雅,并且全套收益不减:

  • 编辑器支持:处处可自动补全,连列表内部元素都有类型提示;
  • 数据转换:解析(parse)与序列化(serialize)递归自动完成,set 自动去重、int 字符串键自动转整型、嵌套 dict 自动转为模型实例;
  • 数据校验:递归穿透每一层嵌套,类型不符、非法 URL、缺必填字段都会被拦截;
  • Schema 文档化:嵌套结构递归生成 JSON Schema / OpenAPI $ref
  • 自动文档:Swagger UI 与 ReDoc 中呈现完整可交互的嵌套请求体结构。

list / set[str] 类型参数、HttpUrl 特殊类型、模型与模型的组合嵌套、纯列表请求体,到任意键类型的 dict[int, float],这些模式组合起来几乎可以表达真实世界中所有请求体形状。所有示例均可在仓库 docs_src/body_nested_models/ 目录下找到并直接运行;对应英文原文位于 docs/en/docs/tutorial/body-nested-models.md,FastAPI 会依据这些模型与类型注解自动完成从解析、校验到文档生成的整条流水线。

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