FastAPI 扩展数据类型指南:UUID、datetime、timedelta、bytes 与 Decimal 在 API 参数中的使用
本篇技术指南聚焦 FastAPI 教程中"扩展数据类型"(Extra Data Types)一节:在 int、float、str、bool 等基础类型之外,如何在路径参数与请求体中使用 UUID、datetime.datetime、datetime.timedelta、frozenset、bytes、Decimal 等更复杂的数据类型,并完整保留编辑器支持、请求数据转换、响应数据转换、数据校验以及自动 OpenAPI 文档生成这五大核心能力。读完后,你将掌握这些类型在请求/响应中的序列化形式、生成的 JSON Schema 格式,以及结合 fastapi/encoders.py 源码与官方测试用例验证行为的方法。
为什么扩展数据类型依然"零成本"可用
在 FastAPI 中,只要你在端点函数签名上写出正确的 Python 类型注解,框架就会借助 Pydantic 自动完成以下工作:
- 优秀的编辑器支持:类型注解本身就是标准 Python 类型,IDE 可以提供补全与检查;
- 请求数据转换:入站请求中的 JSON 数据会被自动解析为对应的 Python 对象;
- 响应数据转换:函数返回值中的 Python 对象会被自动序列化为 JSON 可表示的形式;
- 数据校验:类型不合法时自动返回 422 校验错误;
- 自动标注与文档:OpenAPI Schema 中会为每个类型生成正确的
type与format字段。
这与基础类型的行为完全一致——区别只在于不同复杂类型各自采用什么序列化形式。
常用扩展数据类型及其序列化形式
以下是文档中列出的主要扩展类型及其在请求和响应中的表现:
| Python 类型 | 请求/响应中的表现形式 | 说明 |
|---|---|---|
UUID |
str |
标准"通用唯一识别码"(Universally Unique Identifier),在各类数据库与系统中常用作 ID |
datetime.datetime |
str,ISO 8601 格式 |
例如 2008-09-15T15:53:00+05:00 |
datetime.date |
str,ISO 8601 格式 |
例如 2008-09-15 |
datetime.time |
str,ISO 8601 格式 |
例如 14:23:55.003 |
datetime.timedelta |
float,表示总秒数 |
Pydantic 也支持通过自定义序列化器表示为 ISO 8601 时间差编码 |
frozenset |
与 set 处理相同 |
请求侧读取列表、去重后转为 set;响应侧将 set 转为 list;生成的 Schema 通过 JSON Schema 的 uniqueItems 表明集合值唯一 |
bytes |
str |
生成的 Schema 会标注为带 binary 格式的 str |
Decimal |
与 float 相同 |
标准 Python decimal.Decimal |
此外,所有 Pydantic 支持的合法数据类型在 FastAPI 中均可直接使用——FastAPI 的校验与序列化能力直接建立在 Pydantic 之上,因此 Pydantic 类型系统就是 FastAPI 的类型系统上限。
完整示例:一个使用多种扩展类型的路径操作
下面是仓库 docs_src/extra_data_types/tutorial001_an_py310.py 的完整示例:路径参数使用 UUID,请求体参数分别使用 datetime、timedelta 和可选的 time。
from datetime import datetime, time, timedelta
from typing import Annotated
from uuid import UUID
from fastapi import Body, FastAPI
app = FastAPI()
@app.put("/items/{item_id}")
async def read_items(
item_id: UUID,
start_datetime: Annotated[datetime, Body()],
end_datetime: Annotated[datetime, Body()],
process_after: Annotated[timedelta, Body()],
repeat_at: Annotated[time | None, Body()] = None,
):
start_process = start_datetime + process_after
duration = end_datetime - start_process
return {
"item_id": item_id,
"start_datetime": start_datetime,
"end_datetime": end_datetime,
"process_after": process_after,
"repeat_at": repeat_at,
"start_process": start_process,
"duration": duration,
}
(仓库中同时提供了不使用 Annotated 的等价写法 tutorial001_py310.py,其中通过 start_datetime: datetime = Body() 这类默认值形式声明请求体参数,两者行为一致。)
关键点:函数内部拿到的是"自然类型"
示例中最值得注意的两行是:
start_process = start_datetime + process_after
duration = end_datetime - start_process
函数内部拿到的参数已经是自然的 Python 类型——start_datetime 是真正的 datetime.datetime 对象,process_after 是真正的 timedelta 对象,因此可以直接执行标准的日期算术运算(datetime + timedelta 得到新的 datetime,两个 datetime 相减得到 duration)。开发者不需要手写任何解析或序列化代码,类型转换全部由框架在边界处完成。
源码级佐证:FastAPI 如何序列化这些类型
从源码结构看,响应侧的类型序列化由 fastapi/encoders.py 中的 ENCODERS_BY_TYPE 映射表驱动,该映射明确定义了文档中各类型的输出形式:
ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
datetime.timedelta: lambda td: td.total_seconds(),
Decimal: decimal_encoder,
frozenset: list,
# ...
}
(见 fastapi/encoders.py#L84-L96)
对照文档中的描述,可以逐条印证:
datetime.date/datetime.datetime/datetime.time都走isoformat()函数,输出 ISO 8601 字符串;datetime.timedelta通过total_seconds()输出为总秒数(float),这正是文档中"represented as afloatof total seconds"的实现依据;Decimal走decimal_encoder,无指数时输出int、否则输出float,与文档"handled the same as afloat"一致;frozenset直接转为list输出;bytes通过decode()转为字符串。
也就是说,文档中列出的"类型 → 表现形式"表格并非约定,而是这份编码器映射的直接结果。
请求侧与 OpenAPI Schema 中的格式
请求侧的解析由 Pydantic 完成:字符串形式的 ISO 8601 日期时间、总秒数形式的 timedelta 等都会被校验并转换为对应 Python 类型;OpenAPI Schema 的 format 字段则由 Pydantic 的类型元信息生成。
仓库测试 tests/test_tutorial/test_extra_data_types/test_tutorial001.py 为上述行为提供了可执行的事实依据:
- 端到端请求/响应测试:客户端发送 JSON
{"start_datetime": "2018-12-22T14:00:00+00:00", "end_datetime": "2018-12-24T15:00:00+00:00", "repeat_at": "15:30:00", "process_after": 300},其中process_after: 300正是"timedelta 以总秒数表示"的体现;期望响应中start_process为2018-12-22T14:05:00+00:00(即start_datetime + 300 秒),duration为176100(秒),验证了函数内日期运算与响应序列化的完整闭环; - OpenAPI Schema 快照测试:断言生成的 Schema 中路径参数
item_id为{"type": "string", "format": "uuid"},start_datetime/end_datetime为{"type": "string", "format": "date-time"},repeat_at为anyOf: [string/time, null](体现time | None的可选语义),process_after为{"type": "string", "format": "duration"}——即文档所述"自动标注"能力在这些类型上的具体落地。
实践建议与适用前提
- ID 优先用
UUID:数据库主键、外部资源标识用UUID类型注解,既能获得format: uuid的 Schema 标注,又能在客户端传入非法字符串时自动返回 422 校验错误; - 时间字段用
datetime家族:请求中一律接受 ISO 8601 字符串,响应中自动输出 ISO 8601 字符串,避免手写字符串解析;注意datetime带时区信息时应保持 ISO 8601 时区偏移(如+05:00); - 时长/延迟参数用
timedelta:默认以总秒数float传输。若业务上希望用 ISO 8601 时间差编码(如PT5M)传输,可在 Pydantic 层通过自定义序列化器实现(见文档提示的 Pydantic 序列化文档),FastAPI 本身默认采用总秒数; - 金额等精确计算用
Decimal:避免float的浮点误差,序列化和校验行为与float一致; - 集合去重用
frozenset:请求列表自动去重,Schema 自动标注uniqueItems; - 所有类型必须通过标准类型注解声明,FastAPI 才会同时提供校验、转换与文档生成;这与仓库教程中其他章节(路径参数、查询参数、请求体)的规则一致。
小结
FastAPI 的"扩展数据类型"能力本质上是 Pydantic 类型系统的透传:你在签名中写 UUID、datetime、timedelta、frozenset、bytes、Decimal,框架就在请求边界完成解析与校验、在响应边界通过 fastapi/encoders.py 的编码器映射(或 Pydantic v2 的序列化)完成转换,同时在 OpenAPI 文档中给出带正确 format 的 Schema。结合 示例代码 与 对应测试,你可以直接复制运行并验证每一个类型转换行为。
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 StartedRust0625
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