FastAPI 请求体之嵌套模型(Nested Models):用 Pydantic 构建任意深度的数据模型与校验
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
本篇指南围绕 FastAPI 教程的“Body - Nested Models”展开,讲解如何基于 Pydantic 在请求体中声明list、set、dict以及层层嵌套的模型,从而获得类型提示、数据转换、校验与自动文档。读完你将掌握从简单的tags: list[str]到"模型套模型、模型套列表、纯数组请求体、dict[int, float]任意字典体"的完整写法,并了解仓库源码示例与测试对每一种行为的验证方式。
概述:为什么需要嵌套模型
在编写 API 时,请求体往往不是一层简单的键值对,而是结构化的 JSON 对象。FastAPI基于Pydantic,允许你"定义、校验、文档化并使用任意深度嵌套的模型"——这正是本教程(韩文原文档,内容与 英文文档 一致)的核心主张。
你只要在路径操作函数中把参数类型声明为一个 Pydantic 模型,FastAPI 便会自动完成四项工作:
- 编辑器支持(自动补全等,嵌套模型同样生效);
- 数据转换(解析 / 序列化,即 parsing / serialization);
- 数据校验(非法请求返回带详细错误信息的 422 响应);
- 自动文档(JSON Schema 与 OpenAPI,进而在 Swagger UI 中呈现)。
下文所有的代码示例均来自仓库 docs_src/body_nested_models/,并且每个示例都有对应的自动化测试(见 tests/test_tutorial/test_body_nested_models/)作为行为佐证。
列表字段:先声明"它是一个 list"
最基础的场景是:模型的一个属性是列表。定义一个属性为 Pythonlist类型即可:
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完整代码见 tutorial001_py310.py。
需要注意:tags: list只声明了"tags 是一个列表",并没有声明列表中元素的类型。元素类型留空意味着它可以容纳任意类型的值,校验粒度较粗。若要精细化,就需要用到"带类型参数的列表"。
带类型参数的列表字段:list[str]
Python 中声明"内部有类型"的容器类型(list、dict、tuple等)有专门语法:用方括号[、]把内部类型作为"类型参数"传入,例如:
my_list: list[str]这是标准的 Python 类型声明语法,Pydantic 模型的属性同样遵循。把上面例子的tags精确声明为"字符串组成的列表":
class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None tags: list[str] = []见 tutorial002_py310.py(第 12 行为tags: list[str] = [])。
一旦声明了元素类型,FastAPI/Pydantic 就会对列表中的每一个元素分别做类型转换与校验,并在文档中标注tags是string的数组。
集合类型:用set[str]去重
回到业务思考:标签(tags)通常不应重复,应当是唯一的字符串。Python 恰好有专门表达"唯一元素集合"的类型set。于是把tags声明为字符串集合:
class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None tags: set[str] = set()见 tutorial003_py310.py。
使用set带来三个直接效果(文档原文明确列出):
- 入站去重:即使收到带重复数据的请求,也会被转换成唯一元素的集合;
- 出站去重:每次输出该数据时,即使源头存在重复,也按唯一元素集合输出;
- 按语义标注文档:JSON Schema / OpenAPI 会相应地把该字段标记为
uniqueItems: true的数组。
也就是说,客户端若发送tags: ["rock", "rock", "metal"],服务端最终接收/返回的将是{"rock", "metal"}这类唯一集合。对应验证测试见 test_tutorial001_tutorial002_tutorial003.py。
嵌套模型:模型里面再放一个模型
Pydantic 模型的每个属性都有自己的类型,而这个类型本身可以又是一个 Pydantic 模型。于是你可以用指定的属性名、类型与校验规则,声明出层层嵌套的 JSON"对象",而且嵌套深度任意。
定义子模型
先定义一个小模型Image:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Image(BaseModel): url: str name: str把子模型当作属性类型
再在Item里把image声明为Image | None(可选,允许缺省):
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完整代码见 tutorial004_py310.py(子模型定义在第 7–9 行,嵌套使用在第 18 行)。
这样一来,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模型约束的对象:url与name均必填且会被校验。仅凭一次类型声明,FastAPI 就同时给出编辑器自动补全、数据转换、数据校验与自动文档化。测试见 test_tutorial004.py。
特殊类型与校验:HttpUrl代替str
除了str、int、float这类普通单值类型,你还可以使用从str派生的更复杂单值类型,从而在不写一行自定义校验逻辑的情况下获得额外约束。例如Image.url本是str,可以改声明为 Pydantic 的HttpUrl类型:
from fastapi import FastAPI from pydantic import BaseModel, HttpUrl app = FastAPI() class Image(BaseModel): url: HttpUrl name: str见 tutorial005_py310.py(第 2 行导入HttpUrl,第 8 行使用)。
行为变化:
- 该字符串会被校验是否为合法的 URL;
- 在 JSON Schema / OpenAPI 中会按 URL 格式(
format: uri)正确文档化,Swagger UI 中呈现为专门的 URL 输入框。
FastAPI 文档指出:Pydantic 的类型全貌可参考其官方 Type Overview,后续教程章节还会给出更多示例。本仓库的校验行为可参考 test_tutorial005.py(其中包含了合法 URL 与非法 URL 两类断言)。
子模型组成的列表:list[Image]
Pydantic 模型同样可以作为list、set等容器的元素子类型。把单个image扩展为"多个图像":
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见 tutorial006_py310.py(第 18 行)。此时 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。
深度嵌套模型:模型任意套模型
上述技巧可以自由组合,构成任意深度的嵌套结构。教程给出了三层嵌套的典型例子——Offer拥有若干Item,而每个Item又拥有可选的Image列表:
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见 tutorial007_py310.py(模型行号为 7、12、18、21、25,路径操作在第 28–30 行)。
这里三层结构清晰可读:
Offer→name/description/price/items;items→ 一个Item列表;- 每个
Item→images又是一个可选的Image列表。
声明完全由标准 Python 类型注解驱动,没有额外的配置代码;校验、文档化同样层层生效。测试见 test_tutorial007.py。
纯列表的请求体:list[Image]作顶层参数
有时期望的 JSON 请求体最顶层就是一个数组(Pythonlist),而不包在一个对象里。此时可以像在 Pydantic 模型内部那样,直接在路径操作函数的参数上声明类型:
images: list[Image]示例代码:
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见 tutorial008_py310.py(第 13 行)。请求体形如[{...Image...}, {...Image...}],数组中每个元素都按Image校验,函数参数会收到一个Image实例组成的列表。测试见 test_tutorial008.py。
无处不在的编辑器支持
由于一切均基于类型注解而非手写dict解析,你可以在任何地方获得编辑器补全——连列表内部元素的字段也会被识别:
文档特别对比了两种做法:
- 如果直接用
dict手工处理请求体,就无法获得这种编辑器支持,字段名一旦拼错只能等到运行时才发现; - 但你也不必担心类型声明与运行数据之间的鸿沟:进入的
dict会被自动转换为 Pydantic 模型实例,输出时也会自动序列化回 JSON——两边都是自动的。
任意dict请求体:dict[int, float]
最后一个场景:把请求体声明为"键为某类型、值为另一类型"的字典。与使用 Pydantic 模型不同,这种方式不需要预先知道合法的字段/属性名有哪些——对"希望接收尚不可预知键名"的接口特别有用(例如用户自定义元数据)。
另一个实用场景是想要非字符串键。下面的例子接收任意字典,只要它的键是int、值是float:
from fastapi import FastAPI app = FastAPI() @app.post("/index-weights/") async def create_index_weights(weights: dict[int, float]): return weights见 tutorial009_py310.py(第 7 行为类型声明)。
这里有一个 JSON 与 Python 的重要差异需要牢记(教程中的 tip 明确强调):
JSON 只支持
str作为对象的键。
也就是说,你的 API 客户端只能发送字符串键。但因为 Pydantic 具备自动数据转换能力,只要发送的字符串包含纯整数(如"2"),Pydantic 就会把它转换并校验为int键;最终你在weights参数中拿到的,确实是一个int键、float值的字典。
这一行为在仓库测试中有精确印证——test_tutorial009.py 中:
- 发送
{"2": 2.2, "3": 3.3}返回 200,响应体与原数据一致; - 发送
{"foo": 2.2, "3": 3.3}(键"foo"不是纯整数)则返回422 校验错误,错误项为int_parsing,错误路径定位到["body", "foo", "[key]"],信息为 "unable to parse string as an integer"; - 同时该测试用快照断言了
/index-weights/的 OpenAPI schema:请求体类型为object,additionalProperties为{"type": "number"},印证任意键字典在 OpenAPI 文档中以additionalProperties呈现。
小结
借助FastAPI与 Pydantic,你可以获得模型体系的最大灵活性,同时保持代码简单、简短、优雅,并附带完整的工程收益:
- 编辑器支持(处处自动补全);
- 数据转换(即解析 / 序列化);
- 数据校验;
- Schema 文档化(JSON Schema / OpenAPI);
- 自动交互文档(Swagger UI)。
从"单层对象"到"集合去重"再到"三层嵌套"与"纯数组/任意字典请求体",核心始终是标准 Python 类型注解——FastAPI 负责把注解翻译成校验、转换与文档。所有示例源码集中在 docs_src/body_nested_models/,对应行为测试位于 tests/test_tutorial/test_body_nested_models/,可作为继续实验与回归验证的参考。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考