首页
/ FastAPI 请求体字段校验与元数据:使用 Pydantic `Field` 精确定义模型属性

FastAPI 请求体字段校验与元数据:使用 Pydantic `Field` 精确定义模型属性

2026-09-06 18:10:15作者:邓越浪Henry

在 FastAPI 中为 路径操作函数 的单个参数声明校验与元数据时,你习惯使用 QueryPathBody;而当数据被组织进 Pydantic 模型、作为请求体整体接收时,则可以在模型内部使用 Pydantic 的 Field 为每个属性声明同样的额外校验与元数据。本文以 FastAPI 官方教程 Body - Fields 为骨架,结合本仓库的源码与测试,系统讲解 Field 的导入方式、声明方法、参数能力、与 FastAPI 参数工具共享的底层机制,以及这些声明如何被翻译为 OpenAPI / JSON Schema 元数据。

为什么需要在模型内部声明字段约束

在定义请求体模型时,很多约束只针对模型内部的某个字段,而不是整个请求体。例如:

  • description 是可选的,但若提供则不能超过 300 个字符;
  • price 是必填浮点数,且必须大于 0;
  • tax 可缺省。

这些约束若放在 *路径操作函数* 的参数上用 QueryPathBody 表达并不合适——因为数据被包裹在模型对象里。此时,正确的位置就是 Pydantic 模型的属性声明处,使用的工具则是 Pydantic 的 Field

这一点正是 Body - Fields 的主题:QueryPathBody 为单个参数添加额外校验和元数据一样,用 Pydantic 的 Field 在模型内部为属性添加校验与元数据。

pydantic 导入 Field

首先需要导入 Field。注意:它直接来自 pydantic,而不是像 QueryPathBody 那样来自 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

请留意:Fieldpydantic 导入,而其余参数工具(QueryPathBody 等)均从 fastapi 导入。混淆来源会导致导入错误或行为不符合预期。

上述代码完整保存在仓库的 docs_src/body_fields/tutorial001_py310.py(非 Annotated 写法)与 docs_src/body_fields/tutorial001_an_py310.pyAnnotated 写法)中。两个版本功能完全等价,后者利用 AnnotatedBody(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 的属性,其结构与 *路径操作函数* 的参数完全同构——区别仅在于后者用 PathQueryBody 取代了 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")

理解了这套同构关系,你就掌握了把任何"参数级校验"迁移到"字段级校验"的直觉。

FieldQuery / Path / Body 的底层关系

文档中有一则重要的 Technical Details,它解释了为什么 Field 与 FastAPI 的各个参数工具"长得一模一样":

实际上,QueryPath 以及后续你将见到的其他工具,创建的都是一种公共 Param 类的子类对象,而 Param 类本身又是 Pydantic FieldInfo 类的子类;Pydantic 的 Field 同样返回 FieldInfo 实例;Body 则直接返回 FieldInfo 的某个子类对象。还有更多你稍后会见到的工具是 Body 类的子类。另外请记住:从 fastapi 导入的 QueryPath 等,其实是返回特殊类的函数

源码印证了这一点。在 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 中,PathQueryBody(以及 HeaderCookie 等)都是返回相应 *Info/Param 子类对象的函数。

由此可以推断出完整的继承脉络:

pydantic FieldInfo
 ├── pydantic Field(...) 返回 FieldInfo 实例
 └── fastapi Param(FieldInfo)
      └── Query() / Path() / Header() / Cookie() 等返回 Param 子类
 └── Body(...) 返回 FieldInfo 的(专用)子类对象

因为共享同一套 FieldInfo 基座,Field 天然支持与 QueryPathBody 相同的参数集合——gt/ge/lt/lemin_length/max_lengthpatterntitledescriptionexamplesaliasdeprecatedjson_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 中弃用);
  • aliasvalidation_aliasserialization_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

你在 FieldQueryBody 等中声明的额外信息,都会被纳入最终生成的 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 可以在模型内部为每个属性声明额外的校验与元数据,与 QueryPathBody 为参数做声明的方式完全同构;
  • Field 必须从 pydantic 导入,而非从 fastapi 导入;
  • 技术上,FastAPI 的 Query/Path 等函数返回 ParamFieldInfo 子类)对象,Pydantic 的 Field 返回 FieldInfo 实例,二者共享参数体系(见 fastapi/params.py),因此声明体验一致;
  • Field 中的校验与元数据(titledescriptionmax_lengthgt 等)会如实写入生成的 JSON Schema 与 OpenAPI;
  • 请求违反字段约束时返回 422,错误 loc 精确指向模型内字段路径,相关行为由仓库测试 test_tutorial001.py 验证。

在继续学习 body-nested-models.md 处理嵌套模型、或参考 body.md 回顾请求体基础之前,掌握 Field 是让模型从"容器"升级为"自带契约"的关键一步。

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