FastAPI 请求体字段校验与元数据:使用 PydanticField精确定义模型属性
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
在 FastAPI 中为路径操作函数的单个参数声明校验与元数据时,你习惯使用Query、Path、Body;而当数据被组织进 Pydantic 模型、作为请求体整体接收时,则可以在模型内部使用 Pydantic 的Field为每个属性声明同样的额外校验与元数据。本文以 FastAPI 官方教程 Body - Fields 为骨架,结合本仓库的源码与测试,系统讲解Field的导入方式、声明方法、参数能力、与 FastAPI 参数工具共享的底层机制,以及这些声明如何被翻译为 OpenAPI / JSON Schema 元数据。
为什么需要在模型内部声明字段约束
在定义请求体模型时,很多约束只针对模型内部的某个字段,而不是整个请求体。例如:
description是可选的,但若提供则不能超过 300 个字符;price是必填浮点数,且必须大于 0;tax可缺省。
这些约束若放在*路径操作函数*的参数上用Query、Path、Body表达并不合适——因为数据被包裹在模型对象里。此时,正确的位置就是Pydantic 模型的属性声明处,使用的工具则是 Pydantic 的Field。
这一点正是 Body - Fields 的主题:像Query、Path、Body为单个参数添加额外校验和元数据一样,用 Pydantic 的Field在模型内部为属性添加校验与元数据。
从pydantic导入Field
首先需要导入Field。注意:它直接来自pydantic,而不是像Query、Path、Body那样来自fastapi。
from typing import Annotated from fastapi import Body, FastAPI from pydantic import BaseModel, Field app = FastAPI() class Item(BaseModel): name: str description: str | None = Field( default=None, title="The description of the item", max_length=300 ) price: float = Field(gt=0, description="The price must be greater than zero") tax: float | None = None @app.put("/items/{item_id}") async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): results = {"item_id": item_id, "item": item} return resultswarning
请留意:
Field从pydantic导入,而其余参数工具(Query、Path、Body等)均从fastapi导入。混淆来源会导致导入错误或行为不符合预期。
上述代码完整保存在仓库的 docs_src/body_fields/tutorial001_py310.py(非Annotated写法)与 docs_src/body_fields/tutorial001_an_py310.py(Annotated写法)中。两个版本功能完全等价,后者利用Annotated把Body(embed=True)元信息与类型放在同一处,是当前推荐的现代写法;本仓库对应测试 tests/test_tutorial/test_body_fields/test_tutorial001.py 对两种写法做了参数化回归验证。
使用Field声明模型属性
导入后即可在模型属性上用Field声明额外信息:
class Item(BaseModel): name: str description: str | None = Field( default=None, title="The description of the item", max_length=300 ) price: float = Field(gt=0, description="The price must be greater than zero") tax: float | None = None逐行解读:
| 属性 | 声明 | 含义 |
|---|---|---|
name | name: str | 必填字符串,无额外约束 |
description | str \| None = Field(default=None, title="...", max_length=300) | 可选,默认None;在 JSON Schema 中title被设为 "The description of the item",字符串最大长度 300 |
price | float = Field(gt=0, description="...") | 必填浮点数,gt=0表示严格大于 0;description作为字段说明写入 Schema |
tax | tax: float \| None = None | 可选浮点数,使用普通默认值,未加额外约束 |
在默认值中使用Field的等价写法
description一例演示了把Field(...)整体作为属性默认值的写法(无default=参数的语法在 Pydantic v2 中需显式default=或用Field(...))。两种结构语义一致:
description: str | None = Field(default=None, ...) # 等价于 description: str | None = None # 若不需要任何额外约束与单个参数的声明保持同构
值得注意的一个技巧是:模型中每个带类型、默认值和Field的属性,其结构与*路径操作函数*的参数完全同构——区别仅在于后者用Path、Query、Body取代了Field。对比:
# 路径操作函数参数:用 Query/Path/Body async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): ... # 模型属性:用 Field price: float = Field(gt=0, description="The price must be greater than zero")理解了这套同构关系,你就掌握了把任何"参数级校验"迁移到"字段级校验"的直觉。
Field与Query/Path/Body的底层关系
文档中有一则重要的Technical Details,它解释了为什么Field与 FastAPI 的各个参数工具"长得一模一样":
实际上,
Query、Path以及后续你将见到的其他工具,创建的都是一种公共Param类的子类对象,而Param类本身又是 PydanticFieldInfo类的子类;Pydantic 的Field同样返回FieldInfo实例;Body则直接返回FieldInfo的某个子类对象。还有更多你稍后会见到的工具是Body类的子类。另外请记住:从fastapi导入的Query、Path等,其实是返回特殊类的函数。
源码印证了这一点。在 fastapi/params.py 中可以看到:
class Param(FieldInfo): # type: ignore[misc] in_: ParamTypes即 FastAPI 的Param直接继承自pydantic.fields.FieldInfo(该文件顶部也通过from pydantic.fields import FieldInfo引入)。而在 fastapi/param_functions.py 中,Path、Query、Body(以及Header、Cookie等)都是返回相应*Info/Param子类对象的函数。
由此可以推断出完整的继承脉络:
pydantic FieldInfo ├── pydantic Field(...) 返回 FieldInfo 实例 └── fastapi Param(FieldInfo) └── Query() / Path() / Header() / Cookie() 等返回 Param 子类 └── Body(...) 返回 FieldInfo 的(专用)子类对象因为共享同一套FieldInfo基座,Field天然支持与Query、Path、Body相同的参数集合——gt/ge/lt/le、min_length/max_length、pattern、title、description、examples、alias、deprecated、json_schema_extra等,二者在使用体验上保持一致。
Field的常见参数速查
基于源码 fastapi/params.py 展示的Param.__init__签名,Field(同为FieldInfo体系)支持的常用参数可归纳如下:
数值校验
gt/ge/lt/le:分别约束大于、大于等于、小于、小于等于某数值(price = Field(gt=0));multiple_of:必须是某数的整数倍;allow_inf_nan:是否允许inf/nan;max_digits/decimal_places:用于Decimal类型的小数位数约束。
字符串校验
min_length/max_length:最小 / 最大长度(description = Field(max_length=300));pattern:正则表达式约束(FastAPI 0.100.0 起取代已弃用的regex)。
模型元信息(会写入 JSON Schema / OpenAPI)
title:字段标题(默认取属性名,可覆盖为人类可读的标题,如title="The description of the item");description:字段说明文字;examples/openapi_examples:示例值(example单数形式已在 OpenAPI 3.1 中弃用);alias、validation_alias、serialization_alias:字段别名;deprecated:标记字段废弃;json_schema_extra:向生成的 Schema 追加额外键;discriminator:联合类型的判别字段;strict:启用严格校验模式。
校验生效:422 错误的结构化返回
当请求违反字段约束时,FastAPI 会返回标准的 422 校验错误。仓库测试 tests/test_tutorial/test_body_fields/test_tutorial001.py 用负价格验证了这一点:
def test_invalid_price(client: TestClient): response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) assert response.status_code == 422 assert response.json()["detail"][0] == { "type": "greater_than", "loc": ["body", "item", "price"], "msg": "Input should be greater than 0", "input": -3.0, "ctx": {"gt": 0.0}, }注意错误定位"loc": ["body", "item", "price"]——它精确指出了失败位置处于请求体的item对象内price属性,验证了字段级约束与参数级约束共享同一套校验与错误上报管线。其背后正是 Pydantic v2 的校验器,而 FastAPI 负责把FieldInfo上声明的约束装配进模型字段。
额外信息如何进入生成的 JSON Schema 与 OpenAPI
你在Field、Query、Body等中声明的额外信息,都会被纳入最终生成的 JSON Schema,进而出现在/openapi.json里。同一测试文件中 test_openapi_schema 展示了这一点:Item模型的 Schema 中:
{ "title": "Item", "required": ["name", "price"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, "description": { "title": "The description of the item", "anyOf": [{"maxLength": 300, "type": "string"}, {"type": "null"}] }, "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero" } } }可以清楚看到三处映射:
Field(max_length=300)→"maxLength": 300;Field(gt=0)→"exclusiveMinimum": 0.0;title="The description of the item"、description="The price must be greater than zero"→ Schema 中对应的title/description。
同时,由于请求体使用了Body(embed=True),OpenAPI 中还会额外生成一层Body_update_item_items__item_id__put包装 Schema,把Item嵌套在"item"键下。FastAPI 的交互式文档(/docs)会据此渲染出带字段说明与长度、取值约束的表单,客户端也可依据该 Schema 提前做静态校验。
warning
传入
Field的额外关键字(extra keys)同样会出现在应用最终的 OpenAPI Schema 中。由于这些键不一定属于 OpenAPI 规范本身,部分 OpenAPI 工具(例如 Swagger 官方校验器)可能无法正确解析你生成的 Schema。因此,自定义额外元数据前请权衡其对第三方工具链的兼容性影响。官方文档后续会在讲解 examples(示例)时介绍如何规范地添加这类额外信息。
小结
- 用 Pydantic 的
Field可以在模型内部为每个属性声明额外的校验与元数据,与Query、Path、Body为参数做声明的方式完全同构; Field必须从pydantic导入,而非从fastapi导入;- 技术上,FastAPI 的
Query/Path等函数返回Param(FieldInfo子类)对象,Pydantic 的Field返回FieldInfo实例,二者共享参数体系(见 fastapi/params.py),因此声明体验一致; Field中的校验与元数据(title、description、max_length、gt等)会如实写入生成的 JSON Schema 与 OpenAPI;- 请求违反字段约束时返回 422,错误
loc精确指向模型内字段路径,相关行为由仓库测试 test_tutorial001.py 验证。
在继续学习 body-nested-models.md 处理嵌套模型、或参考 body.md 回顾请求体基础之前,掌握Field是让模型从"容器"升级为"自带契约"的关键一步。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考