FastAPI 系列

  1. FastAPI 系列:从第一个接口到生产部署

  2. FastAPI 为什么适合做 JSON API

  3. 第一个接口怎么跑起来

  4. 路径、查询和请求体为什么要分开声明

  5. Pydantic 模型怎么当契约(当前篇)

  6. 路由怎么拆

  7. 依赖注入到底省了什么

  8. 数据库会话怎么进接口

  9. 状态码和异常为什么不要裸 raise

  10. 鉴权怎么接到 Depends 上

  11. 中间件、CORS 和后台任务分别解决什么

  12. 测试怎么写才不连真实库

  13. 生产怎么部署

JSON 进接口之后,不能还是一堆 dict。Pydantic 模型同时负责校验、文档和响应过滤。入参模型和出参模型必须分开,否则密码、内部标记会顺着 return user 漏出去。

请求 JSON 经 Pydantic 校验后,响应模型再过滤字段

入参模型和出参模型

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, Field

app = FastAPI()


class UserIn(BaseModel):
    username: str = Field(min_length=3, max_length=32)
    password: str = Field(min_length=8)
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


@app.post("/users/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    # 这里应写入哈希后的密码,而不是原样保存
    return user

response_model=UserOut 会在返回前丢掉 password。只写返回类型注解有时不够,显式 response_model 更稳,尤其是返回 ORM 对象或 dict 的时候。

校验失败长什么样

字段不合格时,FastAPI 返回 422,body 是 Pydantic 的错误列表,而不是自己拼的字符串。客户端应按字段名提示用户,不要只弹「失败」。

常用约束:

  • Field(ge=0) 数值范围

  • Field(min_length=1) 字符串长度

  • EmailStr 需要 email-validator

  • model_config = ConfigDict(extra="forbid") 拒绝未知字段

嵌套与列表

class Offer(BaseModel):
    item_id: int
    price: float


class OrderIn(BaseModel):
    user_id: int
    offers: list[Offer]

嵌套模型会原样出现在 OpenAPI 里。前端可以按 schema 生成表单。不要把「任意 JSON」收成 dict[str, Any],除非你真的在做网关透传。

示例写进文档

class Item(BaseModel):
    name: str
    price: float
    model_config = {
        "json_schema_extra": {
            "examples": [
                {"name": "Foo", "price": 42.0}
            ]
        }
    }

Swagger UI 会用这些例子预填。例子要真实,不要写 string / 0 这种占位,否则对接的人会按占位实现。

常见误区

同一个模型既接收密码又返回用户

这是最常见的数据泄漏。拆 UserIn / UserOut / UserInDB

在路径函数里手动 if 校验每个字段

范围、格式、必填应放进模型。路径函数只处理业务规则,例如「库存够不够」。

把 ORM 对象直接 JSON 序列化

关系字段、延迟加载、内部列都会爆。用响应模型,或 Pydantic 的 from_attributes=True 显式转换。

小结

模型是接口契约:进什么、出什么、校验什么。下一篇按模块拆 Router,避免所有路径都堆在 main.py

参考资料