FastAPI-P1:Pydantic模型与参数额外信息

发布于:2025-07-16 ⋅ 阅读:(17) ⋅ 点赞:(0)

欢迎来到啾啾的博客🐱。
记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。
有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。

引言

本篇主要内容:FastAPI常用的Pydantic模型、接口参数声明额外信息和校验做法。
适合快速复习、预览。

资料:

1 Pydantic

FastAPI 是基于 Starlette (Web 部分) 和 Pydantic (数据验证部分) 构建的。
其中,Pydantic 是一款用于 Python 数据验证和设置管理 的库。

简单来说,它的核心作用就是:

  • 把普通的数据(比如字典、JSON)变成结构化的数据对象。
  • 在转换过程中,严格检查数据是否符合你定义的规则。

Pydantic的核心是BaseModel。通过继承它来定义自己的数据模型,这个模型就是告诉Pydantic开发者期望的数据结构。

from pydantic import BaseModel, EmailStr
from typing import Optional

class User(BaseModel):
    name: str
    age: int
    email: Optional[EmailStr] = None

上面这个 User 类就定义了一个模型:

  • 它必须有 name 字段,且类型是 str
  • 它必须有 age 字段,且类型是 int
  • 它有一个可选的 email 字段,类型是 EmailStr(Pydantic 内置的邮箱验证类型),默认值为 None

Pydantic会自动校验模型内容:

data = {
  "name": "张三",
  "age": "25", # 注意这里 age 是字符串
  "email": "zhangsan@example.com"
}

try:
    user = User(**data)
    print(user.name)  # 输出: 张三
    print(user.age)   # 输出: 25 (Pydantic 自动转换为整数)
    print(user.email) # 输出: zhangsan@example.com
    print(user)       # 输出: name='张三' age=25 email='zhangsan@example.com'
except Exception as e:
    print(e)

在这个例子中,Pydantic 做了以下几件事:

  • 它检查了所有字段。
  • 它发现 age 是字符串 "25",但模型要求是 int,于是它 自动完成了类型转换
  • 它甚至检查了 email 的格式,确保它是一个有效的邮箱地址。

与在路径操作函数参数中使用 QueryPathBody 声明额外验证和元数据相同的方式一样,可以在 Pydantic 模型内部使用 Pydantic 的 Field 声明验证和元数据。

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

Field 的工作方式与 QueryPathBody 相同,它具有所有相同的参数等。

1.1 Pydantic的便利

同样是继承,Java 的 Object 类是所有类的最终父类,主要提供了一些基础方法和抽象约定,是面向对象体系的根本标识,目的是为了多态和通用性。

Pydantic的BaseModel继承后,Python Class的管理会简约、便利许多。其则赋予了 Python 类强大的数据管理能力,让数据模型定义、验证和使用变得异常简洁和高效。

  1. 自动化数据验证和类型转换:你不用再手动写大量的 if 语句来检查数据,大大减少了冗余代码。
  2. 清晰的数据模型:你的数据结构一目了然,方便团队协作和代码维护。
  3. 兼容 Python 类型提示:它完美结合了 Python 3.5+ 的类型提示,让你的代码更现代、更易读。
  4. 自动生成文档:Pydantic 模型可以自动生成 OpenAPI (Swagger) 格式的 API 文档,这在构建 API 时非常有用。
  5. 高性能:底层使用 Rust 编写,验证速度非常快,尤其适合处理大量数据。

尽管和Java的Object设计上不一样。但是在继承、组合上与Java对象的用法还是一样的。继承 Pydantic 模型的 Python 类仍然是 Pydantic 模型。 Pydantic 模型可以互相嵌套。

2 为参数声明额外信息和校验

可以通过Query和Annotated来做。

2.1 Query

在 FastAPI 中,Query 是一个专门用于定义 查询参数(Query Parameters) 的函数。

使用 Query 的主要目的是:

  1. 声明参数是查询参数:告诉 FastAPI 这个变量对应的是 URL 中的查询参数。
  2. 设置默认值:如果客户端没有提供这个参数,它会有一个默认值。
  3. 添加额外的验证和元数据:比如最小值、最大长度、描述信息等。
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/products/")
async def read_products(
    # 必需的查询参数,描述为 "产品ID",最小长度1,最大长度50
    product_id: str = Query(..., min_length=1, max_length=50, description="The ID of the product"),
    # 可选的查询参数,默认值是0,描述为 "跳过多少个产品"
    skip: int = Query(0, description="Number of products to skip"),
    # 可选的查询参数,默认值是10,描述为 "获取多少个产品",最小值1,最大值100
    limit: int = Query(10, description="Number of products to retrieve", ge=1, le=100)
):
    return {"product_id": product_id, "skip": skip, "limit": limit}
  • ... (Ellipsis):表示这个参数是必需的,没有默认值。

  • min_length, max_length:字符串长度验证。

  • ge (Greater than or Equal)、le (Less than or Equal):数值范围验证。

  • description:在生成的 OpenAPI 文档中显示这个参数的说明。

  • alias:如果查询参数名和变量名不一致,可以使用 alias 指定。

  • deprecated:标记参数已废弃。

2.2 Annotated

Annotated 是 Python 3.9 引入的一个类型提示工具,位于 typing 模块中。它允许你在类型提示中添加额外的元数据。FastAPI 从 0.95.0 版本开始,强烈推荐使用 Annotated 结合 Query, Path, Body 等函数。

from typing import Annotated, Optional # 导入 Optional 以便演示旧式可选参数
from fastapi import FastAPI, Query, Path, Body

app = FastAPI()

# --- 查询参数 (Query Parameters) ---
@app.get("/items/")
async def read_items(
    # 示例 1: 可选查询参数 (Python 3.10+ 推荐语法: 'str | None')
    # 如果不提供 'q',它将是 None
    q: Annotated[str | None, Query(min_length=3, max_length=50, description="搜索查询字符串")] = None,

    # 示例 2: 带有默认值的可选查询参数
    # 如果不提供 'offset',它将是 0
    offset: Annotated[int, Query(description="跳过的结果数量")] = 0,

    # 示例 3: 必填查询参数 (使用 '...' 明确标记)
    # 客户端必须提供 'limit' 参数
    limit: Annotated[int, Query(ge=1, le=100, description="返回的最大结果数量")] = ...,

    # 示例 4: 旧式可选查询参数 (Python 3.9 及更早版本常见)
    # 如果不提供 'category',它将是 None
    category: Annotated[Optional[str], Query(max_length=20, description="产品类别")] = None,
):
    results = {"q": q, "offset": offset, "limit": limit, "category": category, "items": []}
    # 这里可以根据参数进行数据查询逻辑
    if q:
        results["items"].append({"item_id": "Foo", "name": f"Foo {q}"})
    if category:
        results["items"].append({"item_id": "Bar", "name": f"Bar in {category}"})
    return results

# --- 路径参数 (Path Parameters) ---
@app.get("/users/{user_id}")
async def get_user_by_id(
    # 路径参数默认就是必填的,但我们可以用 Annotated 添加元数据
    user_id: Annotated[int, Path(ge=1, description="用户的唯一ID")]
):
    return {"user_id": user_id, "message": "Fetching user details"}

# --- 请求体 (Request Body) ---
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None

@app.post("/items/")
async def create_item(
    # 整个请求体默认就是必填的,Annotated 用于添加更多信息
    item: Annotated[Item, Body(embed=True, description="要创建的商品数据")]
):
    return {"message": "Item created successfully", "item": item}

Annotated 的优势:

  • 分离关注点:将参数的类型定义和 FastAPI 特定的元数据定义清晰地分离开来。
    • q: str 负责类型提示
    • Query(...) 负责 FastAPI 参数定义和验证
  • 更易读:当你有多个装饰器或复杂的参数定义时,Annotated 让代码结构更清晰。
  • 兼容性:它与 Python 现有的类型检查工具(如 MyPy)更好地配合。

2.3 Path

Path() 用于从 URL 路径中提取参数。这些参数是 URL 本身的一部分,例如 /items/{item_id} 中的 item_id

2.3.1 核心概念与用法

  • 路径参数: 在 FastAPI 路由中,你可以在 URL 路径中使用大括号 {} 定义路径参数。

  • 类型注解: Path() 通常与 Python 的类型注解一起使用,FastAPI 会自动为你进行数据类型转换和验证。

  • 默认值与校验: 你可以使用 Path() 来设置默认值、添加校验规则(如最小/最大长度、最小值/最大值等)以及添加元数据(如描述、示例)。

from fastapi import FastAPI, Path, Query

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(
    # item_id 是路径参数,且是必需的。
    # 使用 `| None` 表示可选,但这里我们希望它是必需的,所以没有 `| None`
    item_id: int = Path(
        ...,  # `...` 表示此参数是必需的 (required)
        title="The ID of the item to get",
        description="Must be an integer greater than 0 and less than 1000",
        ge=1, # Greater than or equal to 1
        lt=1000 # Less than 1000
    ),
    # q 是查询参数,可以是字符串或 None,默认值为 None
    q: str | None = Query(
        None, # 默认值
        min_length=3,
        max_length=50,
        description="Optional query string for the item search"
    )
):
    """
    检索特定 ID 的商品信息。
    """
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

# 💡 运行 FastAPI 应用:
# 保存以上代码为 `main.py`
# 在终端运行: `uvicorn main:app --reload`

# 🌐 访问示例:
# - http://127.0.0.1:8000/items/500  (item_id = 500, q = None)
# - http://127.00.1:8000/items/10?q=somequery (item_id = 10, q = "somequery")
# - http://127.00.1:8000/items/0   (会触发校验错误,因为 item_id 必须 >= 1)

2.4 Body

Body() 用于从 HTTP 请求的请求体中提取数据。这通常用于 POSTPUTPATCH 请求,这些请求通常会发送结构化的数据(如 JSON)。

2.4.1 核心概念与用法

  • 请求体: 客户端向服务器发送的数据,通常以 JSON、XML 或表单数据的形式存在。FastAPI 主要推荐使用 JSON。

  • Pydantic 模型: Body() 最常与 Pydantic 模型一起使用。Pydantic 模型定义了请求体的结构和数据类型,FastAPI 会自动将接收到的 JSON 数据解析并验证为 Pydantic 模型的实例。

  • 单个或多个体参数: 你可以在一个路径操作函数中定义一个或多个 Body() 参数。

  • 默认值与校验: 类似于 Path(),你可以设置默认值和添加校验规则。

from fastapi import FastAPI, Body
from pydantic import BaseModel

app = FastAPI()

# 定义一个 Pydantic 模型来表示请求体的数据结构
class Item(BaseModel):
    name: str
    # description 是可选的字符串,默认值是 None
    description: str | None = None
    price: float
    # tax 是可选的浮点数,默认值是 None
    tax: float | None = None

class User(BaseModel):
    username: str
    full_name: str | None = None

@app.post("/items/")
async def create_item(
    # item 是请求体参数,它是 Item 类型的 Pydantic 模型实例。
    # 同样,`...` 表示这个请求体是必需的。
    item: Item = Body(
        ...,
        title="Item to be created",
        description="Details of the new item including name, description, price, and optional tax.",
        example={ # 为 OpenAPI 文档提供示例
            "name": "Super Laptop",
            "description": "A powerful machine for all your needs.",
            "price": 1500.0,
            "tax": 120.0
        }
    )
):
    """
    创建一个新的商品记录。
    """
    return item

@app.post("/offers/{item_id}")
async def make_offer(
    item_id: int = Path(..., description="The ID of the item for the offer"),
    # item 作为请求体参数,FastAPI 默认会将其解析为 Item 实例
    item: Item,
    # user 明确指定为另一个请求体参数,使用 `Body(...)`
    user: User = Body(
        ...,
        title="User making the offer",
        description="Details of the user making the offer.",
        example={
            "username": "jane.doe",
            "full_name": "Jane Doe"
        }
    )
):
    """
    为一个商品创建报价。
    """
    return {"item_id": item_id, "item": item, "user": user}


# 🌐 测试示例 (使用 Postman, Insomnia 或 cURL):

# 🚀 POST http://127.0.0.1:8000/items/
# Headers: Content-Type: application/json
# Body (JSON):
# {
#     "name": "Wireless Mouse",
#     "description": "Ergonomic design",
#     "price": 25.99
# }
# (tax 字段由于是 `None` 默认值,可以不传)

# 🚀 POST http://127.0.0.1:8000/offers/123
# Headers: Content-Type: application/json
# Body (JSON):
# {
#     "item": {
#         "name": "Cool Gadget",
#         "price": 99.99
#     },
#     "user": {
#         "username": "alice_smith"
#     }
# }