创建时间: 2026-08-25最后更新: 2026-08-27

1. Pydantic

FastAPI 使用 Pydantic 把外部输入转换成符合模型类型和字段约束的 Python 对象。它读取类上的类型提示,完成解析、校验、错误描述和 JSON Schema 生成。

basic-model.py
1
from pydantic import BaseModel
2
3
class ChatRequest(BaseModel):
4
question: str
5
temperature: float = 0.7
6
stream: bool = False

这不是一个普通的数据字典,而是一份数据契约:question 必须存在,temperature 应该能被解析为浮点数,stream 应该能被解析为布尔值。

这一章的案例位于 src/fastapi/api/validation/

两个 HTTP 案例先在 validation/__init__.py 中聚合,并统一声明 /validation 前缀:

validation/__init__.py
1
from fastapi import APIRouter
2
3
from . import fastapi_request, response_model
4
5
router = APIRouter(prefix="/validation", tags=["validation"])
6
router.include_router(fastapi_request.router)
7
router.include_router(response_model.router)

上层 api/router.py 再将验证模块挂载到带有 /api/v1 前缀的版本路由中:

api/router.py
01
from fastapi import APIRouter
02
03
from . import deps, req, validation
04
from .routes import health
05
06
# 需要版本管理的业务接口统一挂载在 /api/v1 下。
07
api_v1_router = APIRouter(prefix="/api/v1")
08
api_v1_router.include_router(health.router)
09
api_v1_router.include_router(deps.router)
10
api_v1_router.include_router(req.router)
11
api_v1_router.include_router(validation.router)
12
13
# 根路由负责聚合不同版本的 API,供应用入口一次性接入。
14
router = APIRouter()
15
router.include_router(api_v1_router)

这样,fastapi_request.py 中定义的相对路径 /chat,最终会组合成 /api/v1/validation/chat。

FastAPI 把它用作请求模型时,会在调用路径函数之前完成校验。只有校验通过,路径函数才会收到 ChatRequest 实例。

下面的案例会向本地 FastAPI 服务发送 POST /api/v1/validation/chat。你可以修改问题、温度和 stream,观察请求模型如何接收并返回解析后的字段:

预览
本地服务:心跳检测中
fastapi_request.py
01
from fastapi import APIRouter
02
from pydantic import BaseModel
03
04
router = APIRouter()
05
06
class ChatRequest(BaseModel):
07
question: str
08
temperature: float = 0.7
09
stream: bool = False
10
11
@router.post("/chat")
12
async def chat(request: ChatRequest) -> dict[str, str | float | bool]:
13
return {
14
"question": request.question,
15
"temperature": request.temperature,
16
"stream": request.stream,
17
}

客户端发送一个 JSON 对象:

request.json
1
{
2
"question": "介绍一下 FastAPI",
3
"temperature": "0.5",
4
"stream": false
5
}

请求进入路径函数时,request.temperature 已经是 float 类型。如果请求缺少 question,或者 temperature 无法转换成数字,FastAPI 会在函数执行前返回校验错误。

2. 解析

日常常把 Pydantic 的行为统称为「校验」,但它默认不只是判断对错,还会尝试把输入解析成目标类型。例如字符串 "20" 可以转换成整数 20。因此更准确的过程是:读取外部数据、尝试转换、执行约束,再得到模型实例。

这也解释了为什么类型提示不是注释。Pydantic 会在运行时读取它们,生成字段规则。转换成功不代表业务一定合理,例如 temperature=1.5 是否允许,仍要通过 Field() 或业务规则明确声明。

3. 实例

Pydantic 模型也可以在 FastAPI 之外直接使用:

model-instance.py
1
request = ChatRequest(
2
question="介绍一下 FastAPI",
3
temperature=0.5,
4
)
5
6
print(request.question)
7
print(request.temperature)
8
print(request.model_dump())

model_dump() 返回普通 Python 字典:

dump-result.py
1
{
2
"question": "介绍一下 FastAPI",
3
"temperature": 0.5,
4
"stream": False,
5
}

Pydantic v2 使用 model_dump(),旧代码中的 .dict() 是 v1 时代的常见写法。阅读旧资料时要注意版本差异。

外部字典或 JSON 字符串通常使用下面两个入口:

validate_input.py
01
from pydantic import BaseModel
02
03
class ChatRequest(BaseModel):
04
question: str
05
06
from_dict = ChatRequest.model_validate({"question": "你好"})
07
from_json = ChatRequest.model_validate_json('{"question":"你好"}')
08
09
print(from_dict.model_dump())
10
print(from_json.model_dump())

model_validate() 接收 Python 对象,model_validate_json() 直接解析 JSON 字符串。对于查询参数、表单等“键和值都是字符串”的数据,还可以使用 model_validate_strings():

validate_strings.py
1
from pydantic import BaseModel
2
3
class Pagination(BaseModel):
4
limit: int
5
6
page = Pagination.model_validate_strings({"limit": "20"})
7
print(page.limit) # 20

model_dump() 返回 Python 数据,model_dump_json() 返回 JSON 字符串。包含 datetime、UUID 等字段时,如果需要得到可以直接交给 JSON 编码器的数据,可以使用 model_dump(mode="json")。名称里的 validate 和 dump 分别对应「进入模型」与「离开模型」。

模型还可以生成 JSON Schema:

schema.py
1
print(ChatRequest.model_json_schema())

FastAPI 会把这些模型信息用于 OpenAPI 文档,因此字段类型和 Field() 约束不仅影响运行时校验,也会影响接口文档。

在 FastAPI 路径函数里通常不必手动调用这些方法,框架已经替你完成请求解析;它们更常用于配置、消息队列、缓存或测试。

如果只需要校验一个列表、字典或普通类型,而不需要创建完整的模型,可以使用 TypeAdapter:

type_adapter.py
1
from pydantic import TypeAdapter
2
3
numbers = TypeAdapter(list[int]).validate_python(["1", 2])
4
print(numbers) # [1, 2]

TypeAdapter 适合复用 Pydantic 的转换和校验能力,但它不会替你定义一个带字段名的 JSON 对象。

4. 必填

是否必填由「有没有默认值」决定,而不只由类型里有没有 None 决定:

required-fields.py
1
from pydantic import BaseModel
2
3
class Example(BaseModel):
4
title: str
5
description: str | None
6
note: str | None = None

三个字段的含义不同:

字段是否必填是否允许 None
title是否
description是是
note否是

description: str | None 表示调用方必须传这个字段,但值可以为 null。只有提供默认值 = None 后,它才成为可省略字段。这是 Pydantic v2 中很容易混淆的地方。

默认值通常代表“没有传入时使用什么”,而不是“这个默认值也要重新校验”。如果默认值来自配置文件、环境变量或其他不可信来源,应显式开启默认值校验:

validate-default.py
1
from pydantic import BaseModel, ConfigDict, Field
2
3
class Settings(BaseModel):
4
model_config = ConfigDict(validate_default=True)
5
6
limit: int = Field(default="20")

对于代码中已经确定的字面量默认值,例如 stream: bool = False,通常不需要额外开启这个选项。

5. Field

Field() 可以声明数值、长度和文档约束:

field-constraints.py
01
from typing import Annotated
02
03
from pydantic import BaseModel, Field
04
05
class ChatRequest(BaseModel):
06
question: Annotated[
07
str,
08
Field(min_length=1, max_length=4000),
09
]
10
temperature: Annotated[
11
float,
12
Field(ge=0, le=2),
13
] = 0.7
14
max_tokens: Annotated[
15
int,
16
Field(gt=0, le=8192),
17
] = 1024

这些限制会同时进入请求校验和 OpenAPI 文档。边界应该来自真实业务或供应商限制,不要随意写一个看似安全的数字。

常用类型

除了 str、int、float 和 bool,Pydantic 还提供了一些常见的业务类型:

common-types.py
01
from datetime import datetime
02
from decimal import Decimal
03
from uuid import UUID
04
05
from pydantic import BaseModel, EmailStr, HttpUrl
06
07
class UserProfile(BaseModel):
08
id: UUID
09
email: EmailStr
10
website: HttpUrl | None = None
11
balance: Decimal
12
registered_at: datetime

UUID、datetime 和 Decimal 会按对应格式解析;EmailStr 和 HttpUrl 会执行更具体的格式检查。使用 EmailStr 需要安装额外依赖:uv add "pydantic[email]"。类型越贴近业务,错误越早暴露,生成的接口文档也越准确。

6. 嵌套

真实请求通常包含嵌套对象和列表:

nested-models.py
01
from typing import Literal
02
03
from pydantic import BaseModel, Field
04
05
class Message(BaseModel):
06
role: Literal["user", "assistant"]
07
content: str = Field(min_length=1)
08
09
class ChatRequest(BaseModel):
10
messages: list[Message] = Field(min_length=1)
11
conversation_id: str | None = None

发送 JSON 后,request.messages 是 Message 对象列表。可以直接访问 request.messages[0].content,不需要在业务代码里反复检查键是否存在。

Literal 把 role 限定在已知取值中。以后增加 system 或 tool 等角色时,应该根据实际协议扩展,而不是先接受任意字符串再在深层逻辑中失败。

当一个字段可能对应多种明确结构时,可以使用联合类型;如果需要根据某个字段选择具体分支,可以使用判别字段:

discriminated-union.py
01
from typing import Annotated, Literal
02
03
from pydantic import BaseModel, Field
04
05
class TextPart(BaseModel):
06
type: Literal["text"]
07
text: str
08
09
class ImagePart(BaseModel):
10
type: Literal["image"]
11
url: str
12
13
ContentPart = Annotated[
14
TextPart | ImagePart,
15
Field(discriminator="type"),
16
]
17
18
class Message(BaseModel):
19
content: ContentPart

这里的 type 不只是普通字段,它告诉 Pydantic 应该使用哪个模型分支校验输入。

7. 默认集合

集合字段建议使用 default_factory 表达「每个实例创建一份新集合」:

default-factory.py
1
from pydantic import BaseModel, Field
2
3
class Conversation(BaseModel):
4
title: str
5
tags: list[str] = Field(default_factory=list)
6
metadata: dict[str, str] = Field(default_factory=dict)

Pydantic 会处理模型默认值,但 default_factory 更清楚地表达所有权,也与普通 Python 类中避免共享可变默认值的习惯一致。

8. 转换

Pydantic 默认会进行一部分合理转换:

conversion.py
1
from pydantic import BaseModel
2
3
class Pagination(BaseModel):
4
limit: int
5
6
page = Pagination.model_validate({"limit": "20"})
7
print(page.limit) # 20

这对 HTTP 很实用,因为查询参数和表单字段原本都是文本。但转换并不意味着任意数据都会被接受,也不应该依赖模糊转换表达关键业务规则。

需要区分三种常见入口:model_validate() 处理已经解析好的 Python 对象,model_validate_json() 处理 JSON 文本,model_validate_strings() 则把字典中以字符串表示的值按字段类型解析。它们都默认使用宽松模式,但“宽松”不等于无限制。例如整数通常可以从数字字符串转换,却不能从任意单词转换;布尔值也可能接受 "true"、"0" 等约定格式。

需要严格类型时,可以开启严格模式:

strict-model.py
1
from pydantic import BaseModel, ConfigDict
2
3
class StrictPagination(BaseModel):
4
model_config = ConfigDict(strict=True)
5
6
limit: int

严格模式也可以只作用于一次校验或一个字段:

strict_levels.py
01
from typing import Annotated
02
03
from pydantic import BaseModel, Field, ValidationError
04
05
class Request(BaseModel):
06
limit: Annotated[int, Field(strict=True)]
07
08
try:
09
Request.model_validate({"limit": "20"}, strict=True)
10
except ValidationError as error:
11
print(error.errors())

是否严格应根据数据来源决定。HTTP 查询字符串天然是文本,全面严格可能反而难用;内部事件、配置或金额等敏感数据则适合更严格的契约。严格模式是输入边界的选择,不是“校验级别越高越好”。

9. 额外字段

默认情况下,模型会忽略未声明的额外字段(extra="ignore")。对外部 API,希望尽早发现拼写错误时,可以禁止额外字段:

forbid-extra.py
1
from pydantic import BaseModel, ConfigDict
2
3
class ChatRequest(BaseModel):
4
model_config = ConfigDict(extra="forbid")
5
6
question: str

如果客户端误传 qusetion,接口会直接报告未知字段,而不是悄悄丢弃它。不过对需要向前兼容的协议,完全禁止额外字段也可能让旧客户端过于脆弱,需要结合版本策略决定。

extra 有三个常用策略:ignore(默认,忽略额外字段)、forbid(遇到额外字段时报错)和 allow(保留额外字段)。如果选择 allow,额外数据会放在模型的 model_extra 中;这适合需要透传扩展字段的协议,但业务代码应该明确哪些字段可以被信任。

模型创建后再次赋值时,默认不会重新校验。配置或编辑场景需要这个能力时,可以开启 validate_assignment:

validate_assignment.py
01
from pydantic import BaseModel, ConfigDict, Field, ValidationError
02
03
class Settings(BaseModel):
04
model_config = ConfigDict(validate_assignment=True)
05
06
limit: int = Field(ge=1)
07
08
settings = Settings(limit=10)
09
10
try:
11
settings.limit = 0
12
except ValidationError as error:
13
print(error.errors())

10. 校验器

字段间或内容级规则可以使用校验器:

field-validator.py
01
from pydantic import BaseModel, field_validator
02
03
class ChatRequest(BaseModel):
04
question: str
05
06
@field_validator("question")
07
@classmethod
08
def validate_question(cls, value: str) -> str:
09
normalized = value.strip()
10
11
if not normalized:
12
raise ValueError("问题不能为空")
13
14
return normalized

校验器返回的值会成为字段最终值,因此这里顺便去掉了首尾空白。校验器适合数据自身的规则,不适合执行数据库查询、模型调用或其他昂贵 IO。

如果同一条规则会被多个模型复用,可以把它提取为 Annotated 类型。默认的 AfterValidator 会在字段已经完成类型解析后执行;需要先处理原始输入时,则可以使用 BeforeValidator。

reusable-validator.py
01
from typing import Annotated
02
03
from pydantic import AfterValidator, BaseModel
04
05
def non_empty(value: str) -> str:
06
value = value.strip()
07
if not value:
08
raise ValueError("文本不能为空")
09
return value
10
11
NonEmptyText = Annotated[str, AfterValidator(non_empty)]
12
13
class Message(BaseModel):
14
content: NonEmptyText

涉及多个字段时,可以使用模型级校验器,但应先判断规则是否真的属于数据结构。有些约束属于业务服务,而不是 Pydantic 模型。

model-validator.py
01
from typing import Self
02
03
from pydantic import BaseModel, model_validator
04
05
class GenerationOptions(BaseModel):
06
min_tokens: int = 1
07
max_tokens: int = 1024
08
09
@model_validator(mode="after")
10
def validate_token_range(self) -> Self:
11
if self.min_tokens > self.max_tokens:
12
raise ValueError("min_tokens 不能大于 max_tokens")
13
14
return self

字段校验器适合单个字段,模型校验器适合字段之间的关系。mode="after" 表示先完成字段解析,再基于已经生成的模型检查整体规则。mode="before" 可以在解析前清理或改写原始输入,但此时拿到的可能是字典、字符串等不同形态的数据。不要在校验器中修改数据库或调用外部 API,因为校验可能在多个模型边界重复发生,也会让测试和重试变得不可预测。

11. 别名

外部协议的字段名不一定符合 Python 风格。可以用别名把二者分开:

field-alias.py
1
from pydantic import BaseModel, ConfigDict, Field
2
3
class ChatRequest(BaseModel):
4
model_config = ConfigDict(
5
validate_by_alias=True,
6
validate_by_name=True,
7
)
8
9
max_tokens: int = Field(alias="maxTokens", ge=1)

客户端可以发送 maxTokens,代码内部继续使用 request.max_tokens。validate_by_alias=True 允许使用外部别名,validate_by_name=True 允许使用 Python 字段名。两者同时开启,就能兼容两种输入写法;如果只希望协议使用别名,可以关闭 validate_by_name。旧资料中常见的 populate_by_name=True 仍然值得认识,但新代码更适合明确表达这两个方向的验证策略。

导出时是否使用别名要显式决定:request.model_dump(by_alias=True) 会得到 {"maxTokens": ...}。一旦字段名已经发布,别名就是兼容性策略的一部分,不应随意更改。

12. 输入输出

不要让同一个模型同时承担数据库记录、外部请求和公开响应的全部职责。输入和输出经常需要不同字段:

separate-models.py
01
from datetime import datetime
02
03
from pydantic import BaseModel
04
05
class ConversationCreate(BaseModel):
06
title: str
07
08
class ConversationRead(BaseModel):
09
id: str
10
title: str
11
created_at: datetime

客户端创建会话时不应该自行指定服务器生成的 ID 和时间。拆开模型能明确谁负责提供每个字段,也能防止内部字段意外进入响应。

在 FastAPI 路径函数上声明 response_model,就可以把这个边界落实到接口:

下面的案例会向本地 FastAPI 服务发送 POST /api/v1/validation/conversations。你可以修改会话标题,观察响应模型如何只返回公开字段,并过滤服务端内部字段:

预览
本地服务:心跳检测中
response_model.py
01
from datetime import datetime
02
03
from fastapi import APIRouter
04
from pydantic import BaseModel
05
06
router = APIRouter()
07
08
class ConversationCreate(BaseModel):
09
title: str
10
11
class ConversationRead(BaseModel):
12
id: str
13
title: str
14
created_at: datetime
15
16
@router.post("/conversations", response_model=ConversationRead)
17
async def create_conversation(request: ConversationCreate) -> dict[str, object]:
18
return {
19
"id": "conversation-1",
20
"title": request.title,
21
"created_at": datetime.now(),
22
"internal_note": "只供服务端调试",
23
}

response_model 会校验并过滤路径函数的返回值:internal_note 不会出现在公开响应中,created_at 也会按响应模型的定义序列化。它不是只用于生成文档,而是响应边界的一部分。对于不希望让 FastAPI 从返回值推断响应模型的场景,也可以使用返回类型标注或单独声明 response_model。

13. 错误

当请求不符合模型时,FastAPI 默认返回 422,并给出结构化错误位置。例如假设 temperature 声明为 Field(ge=0),客户端发送 {"temperature": -1},响应可能是:

request-validation-error.json
01
{
02
"detail": [
03
{
04
"type": "greater_than_equal",
05
"loc": ["body", "temperature"],
06
"msg": "Input should be greater than or equal to 0",
07
"input": -1,
08
"ctx": {"ge": 0}
09
}
10
]
11
}

loc 的第一段通常表示请求来源,例如 body、path、query 或 header,后面依次是字段名、数组下标或嵌套路径。前端可以根据它定位表单控件,服务端也可以据此记录可读的错误日志。

如果在 FastAPI 外直接使用 Pydantic,可以通过 ValidationError.errors() 获取相同风格的结构化信息:

pydantic_errors.py
1
from pydantic import BaseModel, ValidationError
2
3
class User(BaseModel):
4
age: int
5
6
try:
7
User(age="not-a-number")
8
except ValidationError as error:
9
print(error.errors())

此时 loc 只包含模型字段,不会有 FastAPI 添加的 body 或 query 前缀。

这类错误属于请求结构校验,通常不需要在每个路径函数中手动捕获。后面的异常文章会讲如何定制统一错误协议,以及如何区分校验错误、业务错误和服务异常。

请求校验失败返回 422;响应模型校验失败则通常表示服务端返回了错误结构,FastAPI 会把它作为服务器错误处理,而不是把内部校验细节原样暴露给客户端。两者虽然都使用 Pydantic,责任方不同。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容