FastAPI 请求体字段校验与元数据:使用 Pydantic `Field` 精确定义模型属性
在 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 results
warning
请留意:
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 是让模型从"容器"升级为"自带契约"的关键一步。
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