FastAPI 嵌套模型请求体全解:从 list/set 字段到任意深度的 Pydantic 模型
在开发 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 模型并声明 tags 为 list:
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 后你会获得三个层面的效果:
- 入站转换:即使请求中带重复数据(例如
["rock", "metal", "rock"]),也会被转换成唯一元素集合; - 出站序列化:无论数据源内部是否有重复,输出响应时都以集合(唯一元素)形式返回;
- 文档标注: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 为例
除了 str、int、float 等常规单一类型,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 模型同样可以作为 list、set 等容器的内部元素类型(源码 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])再放进 Offer 的 items 列表中,就构成了三层结构(源码 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_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 会依据这些模型与类型注解自动完成从解析、校验到文档生成的整条流水线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
