首页
/ FastAPI 高级数据类型的完全指南:UUID、datetime、bytes、Decimal 等

FastAPI 高级数据类型的完全指南:UUID、datetime、bytes、Decimal 等

2026-09-07 16:48:33作者:范垣楠Rhoda

本文是 FastAPI 教程系列中的进阶篇,讲解如何在 path operation(路径操作)的参数、请求体与响应中使用比 intstr 等更丰富的 Python 数据类型。你将掌握 UUIDdatetime.datetime / date / time / timedeltafrozensetbytesDecimal 等在请求解析、数据校验、JSON 序列化与 OpenAPI 文档自动生成中的具体行为,并看到经过测试验证的真实运行效果。读完即可在自己的接口中放心地使用这些类型处理带时区的日期、数据库主键 ID、二进制数据与高精度数值。

为什么需要"额外数据类型"

到目前为止,绝大多数示例都只用到几种最基础的 Python 类型:

  • int:整数
  • float:浮点数
  • str:字符串
  • bool:布尔值

但在真实业务里,这些类型远远不够。例如:

  • 数据库中的主键往往是 UUID(通用唯一标识符);
  • 任务的创建时间、截止时间需要精确到带时区的 datetime
  • 某些字段只关心日期或只关心一天中的时刻;
  • 性能统计需要表示"持续了多长时间"的 timedelta
  • 上传内容的 SHA 校验值或原始字节需要用 bytes
  • 金额计算需要精度可控的 Decimal

好消息是:FastAPI 声明这些类型并不需要额外配置。只要你在类型注解中直接写出这些 Python 类型,就能免费获得与基础类型完全一致的四项能力:

  1. 优秀的编辑器支持(自动补全与静态类型检查);
  2. 入站请求数据的自动转换(字符串 → 真正的 Python 对象);
  3. 出站响应数据的自动转换(Python 对象 → JSON 兼容格式);
  4. 自动的数据校验与 OpenAPI 注解、交互式文档生成。

这套机制由 FastAPI 声明参数类型的 Pydantic 数据模型与 JSON Schema 生成 能力,配合 JSON 兼容编码器 共同完成。

支持的数据类型一览与 JSON 表示规则

下面逐一说明这些"额外数据类型"在网络传输层(JSON 请求/响应)的表示方式。理解这一点是使用它们的关键——JSON 本身并没有日期、二进制、UUID 这些概念,所以框架必须在字符串/数字与 Python 对象之间建立明确的双向映射。

UUID

  • 标准的"通用唯一标识符",许多数据库和系统用它作为 ID。
  • 在请求和响应中均表示为字符串(如 "ff97dd87-a4a5-4a12-b412-cde99f33e00e")。
  • 校验时会检查字符串是否真的是合法的 UUID 格式,非法值会触发 422 校验错误。

datetime.datetime / datetime.date / datetime.time

三者都对应 Python 标准库 datetime 中同名类型:

  • datetime.datetime:在请求和响应中表示为 ISO 8601 格式字符串,例如 2008-09-15T15:53:00+05:00(可带时区偏移)。
  • datetime.date:仅表示日期,表示为 ISO 8601 日期字符串,例如 2008-09-15
  • datetime.time:仅表示一天中的时刻,表示为 ISO 8601 时间字符串,例如 14:23:55.003(可含毫秒)。

在 OpenAPI 生成的 JSON Schema 中,它们分别对应 type: stringformat: date-time / format: date / format: time(时间与日期时间的对照可在 测试用例 的 OpenAPI 快照中看到)。

datetime.timedelta

  • 表示一段持续时间。
  • 在请求和响应中表示为表示总秒数的 float。例如测试数据中 process_after: 300 就代表 300 秒(5 分钟)。
  • Pydantic 还支持将其序列化为"ISO 8601 时间差编码"(如 PT5M),这一点可查看 Pydantic 官方文档的序列化(custom serializers)部分。在 OpenAPI Schema 中其 format 显示为 duration(见测试快照)。

frozenset

  • set 完全同等对待:
    • 在请求中,读取一个数组(list),去重后转换为 setfrozenset 同理);
    • 在响应中,set / frozenset 会转换回数组(list);
    • 生成的 JSON Schema 会使用 uniqueItems: true 声明集合中的值唯一。

注意 FastAPI 约定返回 set 时统一转换为 list,这正是 JSON 可序列化性所要求的——JSON 本身没有"集合"这一数据结构,只有数组。

bytes

  • 标准 Python bytes
  • 在请求和响应中按字符串处理(如二进制内容经过 decode 得到文本)。
  • 生成的 JSON Schema 声明为 type: stringformat: binary
  • 若想处理真正的文件上传(multipart),应配合 request files 使用 UploadFile / File

Decimal

  • Python 标准 decimal.Decimal
  • 在请求和响应中与 float 同等处理(按十进制精确解析,响应时再编码为 JSON 数值)。
  • 适合金额等对精度敏感的场景。

以上类型并非全部。Pydantic 还支持 IP 地址、URL、颜色、枚举等更多数据类型,完整列表见 Pydantic 官方数据类型文档。

源码视角:这些转换究竟发生在哪

理解"请求字符串如何变成 Python 对象、返回的 Python 对象如何变成 JSON",可以看 FastAPI 的 JSON 兼容编码器实现 fastapi/encoders.py。其中定义了类型到编码函数的映射表 ENCODERS_BY_TYPE

  • byteslambda o: o.decode()(字节串解码为普通字符串);
  • datetime.datedatetime.datetimedatetime.timeisoformat(即 o.isoformat(),产出 ISO 8601 文本);
  • datetime.timedeltalambda td: td.total_seconds()(换算成总秒数,这正解释了"请求/响应中 timedelta 是 float 秒数");
  • Decimaldecimal_encoder(指数为非负整数时转为 int,否则转为 float,避免精度在 JSON 中丢失);
  • frozensetsetlist(集合转数组);
  • UUIDstr

jsonable_encoder 会在 FastAPI 发送任何响应前运行(内部也用于 Pydantic 模型 model_dump(mode="json") 之后),保证所有返回值都是 JSON 可序列化的。也就是说:你在函数体里做的任何类型运算(如日期相加、求差),最终返回时都会经过这张映射表得到 JSON 兼容结果

完整示例:一个同时使用多种额外类型的接口

带类型注解的推荐写法(使用 Annotated

文档主推的现代写法在 docs_src/extra_data_types/tutorial001_an_py310.py

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,
    }

代码要点:

  • item_id: UUID 放在路径中。FastAPI 会先校验路径段是合法 UUID,再传入函数,所以函数体里拿到的是真正的 UUID 对象。
  • start_datetimeend_datetimeprocess_after 与可选的 repeat_at 都通过 Body() 声明为请求体字段。带时区的 ISO 8601 字符串进入函数前已被解析成真实的 datetime / time / timedelta 对象。
  • repeat_attime | None 表示可为空,且带默认值 None,因此请求体中可以省略它(OpenAPI 中对应的 Schema 是 anyOf: [{"type": "string", "format": "time"}, {"type": "null"}],见测试快照)。
  • 函数体内的参数都是"原生 Python 类型",可以直接做常规运算:start_datetime + process_after(把开始时刻加上处理时长得到真正开始处理的时刻),以及 end_datetime - start_process(求时间差,得到一个 timedelta,最终以总秒数形式出现在响应中)。

不使用 Annotated 的等价写法

仓库还提供了旧式写法 docs_src/extra_data_types/tutorial001_py310.py,二者行为完全一致:

from datetime import datetime, time, timedelta
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: datetime = Body(),
    end_datetime: datetime = Body(),
    process_after: timedelta = Body(),
    repeat_at: time | None = Body(default=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,
    }

两种写法差别仅在元数据的携带方式上:前者用 AnnotatedBody() 与类型绑定,后者用默认值传入 Body();实际效果相同。仓库测试 test_tutorial001.py 用参数化(pytest.param("tutorial001_py310")pytest.param("tutorial001_an_py310"))把两个文件都跑了一遍,验证行为一致(示例使用 Python 3.10 及以上语法,测试以 needs_py310 标记约束运行环境)。

实际请求与响应示例

参照测试用例中的请求数据构造一次调用(接口为 PUT /items/{item_id}):

请求(路径与 JSON 请求体):

PUT /items/ff97dd87-a4a5-4a12-b412-cde99f33e00e
{
  "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
}

观察几个细节:

  • item_id 是带连字符的 UUID 字符串;
  • 两个日期时间带时区偏移 +00:00
  • repeat_at 只给了时刻;
  • process_after 直接给了数值秒数 300(对应 5 分钟)。

响应(测试断言 response.json() 完全等于以下内容):

{
  "item_id": "ff97dd87-a4a5-4a12-b412-cde99f33e00e",
  "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,
  "start_process": "2018-12-22T14:05:00+00:00",
  "duration": 176100
}

据此可以直观地验证两条"计算"逻辑:

  • start_process(真正开始处理的时间)= start_datetime 加 5 分钟 → 14:05
  • duration = end_datetimestart_process。两天零一小时再减去 5 分钟后,时差为 48 小时 55 分钟 = 176100 秒,以 float 总秒数(这里是整数)输出,这正是 timedelta 的 JSON 编码规则。

自动生成的 OpenAPI 文档行为

启动应用后,访问自动文档(FastAPI 的 /docs,Swagger UI),路径参数与请求体的字段类型声明均来自类型注解。测试对 /openapi.json 做了完整快照断言,其中关键片段为:

  • 路径参数 item_id{"type": "string", "format": "uuid"}
  • 请求体字段 start_datetime / end_datetime{"type": "string", "format": "date-time"}
  • 请求体字段 repeat_at{"anyOf": [{"type": "string", "format": "time"}, {"type": "null"}]}(因可为空且非必填,因此不在 required 数组中);
  • 请求体字段 process_after{"type": "string", "format": "duration"}
  • required 数组为 ["start_datetime", "end_datetime", "process_after"]repeat_at 因有默认值而可选)。

也就是说:交互式文档不仅提示字段名,还会自动标注每个字段的格式(uuid / date-time / duration / binary),并提供对应的输入校验,客户端开发与前后端联调因此可以直接复用这份契约。

补充:请求体中声明"非模型"字段

本例没有定义 Pydantic BaseModel,而是直接在函数签名里用 Body() 声明多个请求体字段。FastAPI 会为这种情况自动生成一个隐式的请求体模型(OpenAPI 中表现为名为 Body_read_items_items__item_id__put 的组件 Schema,可从测试快照中确认)。当你只需要一组松散字段、不希望单独建模型时,这是一种轻量写法;字段更多或结构复杂时,则更适合定义 请求体模型 以复用结构。

小结

  • FastAPI 的额外数据类型支持"零配置、全自动":解析、校验、序列化、OpenAPI 文档由类型注解 + 底层 Pydantic / JSON 编码器共同完成。
  • 网络层只认字符串与数字:UUID 与日期时间走 ISO 文本,timedelta 走总秒数,set / frozenset 走数组,bytesbinary 格式字符串。
  • 响应编码规则集中在 fastapi/encoders.pyENCODERS_BY_TYPE 映射表中,函数体内可放心使用原生类型做日期加减等运算。
  • 仓库的示例代码位于 docs_src/extra_data_types/,配套的端到端测试位于 tests/test_tutorial/test_extra_data_types/test_tutorial001.py,包含运行时行为与 OpenAPI Schema 的完整断言,可作为理解与验证这些规则的第一手依据。

后续若想把这些类型放进 Pydantic 模型字段(响应模型、请求体模型),规则完全一致,可进一步阅读 请求体字段与模型响应模型JSON 兼容编码器

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