请求体与Pydantic

写给初学者:每个概念都从”为什么需要它”开始讲,配合通俗比喻和完整可运行代码。


一、请求体(Request Body)

1.1 什么是请求体?

路径参数和查询参数都放在 URL 里,但有些数据太复杂或太敏感,不适合放在 URL 里。比如:

 
{
 
  "username": "张三",
 
  "email": "zhangsan@qq.com",
 
  "age": 25
 
}
 

这种数据放在 HTTP 请求的 body(请求体) 里,格式是 JSON。

1.2 用 Pydantic 模型接收请求体

 
from fastapi import FastAPI
 
from pydantic import BaseModel
 
app = FastAPI()
 
# 继承 BaseModel,声明每个字段的类型
 
class UserCreate(BaseModel):
 
    username: str
 
    email: str
 
    age: int
 
# 参数类型是 BaseModel 的子类 → FastAPI 自动从请求体取数据
 
@app.post("/users")
 
async def create_user(user: UserCreate):
 
    return {
 
        "username": user.username,
 
        "email": user.email,
 
        "age": user.age,
 
        "msg": "用户创建成功"
 
    }
 

用 curl 测试:

 
curl -X POST "http://127.0.0.1:8000/users" \
 
  -H "Content-Type: application/json" \
 
  -d '{"username": "张三", "email": "zhangsan@qq.com", "age": 25}'
 

返回:

 
{"username": "张三", "email": "zhangsan@qq.com", "age": 25, "msg": "用户创建成功"}
 

1.3 FastAPI 怎么判断参数从哪来?

判断规则(从左到右依次检查):

条件数据来源例子
参数名在路径里 {xxx}路径参数@app.get("/users/{user_id}")user_id
参数类型是 BaseModel 子类请求体user: UserCreate
其他情况查询参数skip: int = 0
 
@app.put("/users/{user_id}")
 
async def update_user(
 
    user_id: int,           # 在路径里 → 路径参数
 
    user: UserCreate,       # BaseModel 子类 → 请求体
 
    notify: bool = False    # 其他 → 查询参数
 
):
 
    return {
 
        "user_id": user_id,      # 来自 URL 路径: /users/42
 
        "username": user.username, # 来自请求体 JSON
 
        "notify": notify           # 来自 URL 查询: ?notify=true
 
    }
 

请求示例:

 
PUT /users/42?notify=true
 
Body: {"username": "张三", "email": "zhangsan@qq.com", "age": 25}
 

1.4 请求体 + 路径参数 + 查询参数混用

这是实际开发中最常见的场景:

 
from fastapi import FastAPI, Path, Query
 
from pydantic import BaseModel
 
app = FastAPI()
 
class ItemCreate(BaseModel):
 
    name: str
 
    price: float
 
    description: str = ""  # 有默认值,可选
 
@app.post("/users/{user_id}/items")
 
async def create_item_for_user(
 
    user_id: int,                    # 路径参数
 
    item: ItemCreate,                # 请求体
 
    category: str = "未分类",         # 查询参数(可选)
 
    urgent: bool = False             # 查询参数(可选)
 
):
 
    return {
 
        "user_id": user_id,
 
        "item_name": item.name,
 
        "item_price": item.price,
 
        "description": item.description,
 
        "category": category,
 
        "urgent": urgent
 
    }
 

请求示例:

 
POST /users/42/items?category=电子&urgent=true
 
Body: {"name": "键盘", "price": 299.0, "description": "机械键盘"}
 

返回:

 
{
 
  "user_id": 42,
 
  "item_name": "键盘",
 
  "item_price": 299.0,
 
  "description": "机械键盘",
 
  "category": "电子",
 
  "urgent": true
 
}
 

二、Query / Path / Body / Field 显式声明(进阶)

2.1 为什么需要显式声明?

当 FastAPI 自动判断不够精确时,你可以用 Query()Path()Body()Field() 显式声明参数来源和约束。

 
from fastapi import FastAPI, Query, Path, Body
 
from pydantic import BaseModel, Field
 
app = FastAPI()
 

2.2 Query —— 给查询参数加约束

 
@app.get("/items")
 
async def list_items(
 
    # Query() 可以加更多约束
 
    q: str = Query(
 
        ...,                    # 必填
 
        min_length=3,           # 最短 3 个字符
 
        max_length=50,          # 最长 50 个字符
 
        description="搜索关键词"  # 文档里的描述
 
    ),
 
    skip: int = Query(0, ge=0),          # 大于等于 0
 
    limit: int = Query(10, ge=1, le=100) # 1 到 100 之间
 
):
 
    return {"q": q, "skip": skip, "limit": limit}
 
请求结果
/items?q=py❌ 422:q 只有 2 个字符,不满足 min_length=3
/items?q=python✅ 正常
/items?q=python&skip=-1❌ 422:skip 必须 >= 0
/items?q=python&limit=200❌ 422:limit 必须 <= 100

2.3 Path —— 给路径参数加约束

 
@app.get("/users/{user_id}")
 
async def get_user(
 
    user_id: int = Path(..., gt=0, description="用户 ID,必须大于 0")
 
):
 
    return {"user_id": user_id}
 

2.4 Body —— 给请求体加额外信息

 
class Item(BaseModel):
 
    name: str
 
    price: float
 
class User(BaseModel):
 
    username: str
 
    email: str
 
@app.put("/items/{item_id}")
 
async def update_item(
 
    item_id: int,
 
    item: Item,                                    # 请求体(自动识别)
 
    user: User = Body(...),                         # 另一个请求体字段
 
    importance: int = Body(..., gt=0)               # 请求体里的单个字段
 
):
 
    return {
 
        "item_id": item_id,
 
        "item": item.model_dump(),
 
        "user": user.model_dump(),
 
        "importance": importance
 
    }
 

请求示例:

 
{
 
  "item": {"name": "键盘", "price": 299},
 
  "user": {"username": "张三", "email": "zhangsan@qq.com"},
 
  "importance": 5
 
}
 

2.5 Field —— 给模型字段加约束

Field() 定义在 BaseModel 内部,用来约束字段本身的长度、范围、描述等。

 
from pydantic import BaseModel, Field
 
class UserCreate(BaseModel):
 
    # ... 表示必填
 
    # min_length=3:最少 3 个字符
 
    # max_length=20:最多 20 个字符
 
    username: str = Field(..., min_length=3, max_length=20, description="用户名")
 
    # gt=0:greater than 0,大于 0
 
    # le=150:less than or equal 150,小于等于 150
 
    age: int = Field(..., gt=0, le=150, description="年龄")
 
    # 有默认值就变成可选字段
 
    email: str = Field(default="未填写", description="邮箱")
 

Field 常用参数速查:

参数含义例子
...必填字段Field(...)
default默认值Field(default="未知")
min_length字符串最短长度Field(min_length=3)
max_length字符串最长长度Field(max_length=20)
gtgreater than(大于)Field(gt=0)
gegreater than or equal(大于等于)Field(ge=0)
ltless than(小于)Field(lt=100)
leless than or equal(小于等于)Field(le=100)
description描述(显示在 API 文档里)Field(description="用户名")

测试效果:

 
# ✅ 合法
 
user = UserCreate(username="张三", age=25)
 
# ❌ 报错:username 只有 2 个字符
 
user = UserCreate(username="ab", age=25)
 
# ❌ 报错:age 不在范围内
 
user = UserCreate(username="张三", age=200)
 
# → ValidationError: Input should be less than or equal to 150
 

2.6 Field vs Body 的区别

这两个容易搞混,但用途完全不同:

Field()Body()
定义位置BaseModel 内部函数参数上
作用约束字段本身(长度、范围、描述)声明参数从请求体取
影响范围模型在哪用都生效只影响当前函数参数
典型场景所有请求体都需要校验多个 BaseModel 混用或单个字段从 body 取
 
from fastapi import FastAPI, Body
 
from pydantic import BaseModel, Field
 
app = FastAPI()
 
# 不管这个模型用在哪个接口,username 都必须 3-20 个字符
 
class UserCreate(BaseModel):
 
    username: str = Field(..., min_length=3, max_length=20)
 
    email: str
 
    age: int = Field(..., gt=0, le=150)
 
@app.post("/users")
 
async def create_user(user: UserCreate):
 
    return user
 
# 当你需要从请求体里取单个字段(不是整个 BaseModel),用 Body() 告诉 FastAPI
 
@app.put("/users/{user_id}")
 
async def update_user(
 
    user_id: int,
 
    user: UserCreate,                          # BaseModel → 自动识别为请求体
 
    admin_note: str = Body(..., min_length=1),  # 单个字段 + Body() → 从请求体取
 
    priority: int = Body(default=0, ge=0, le=5) # 单个字段 + Body() → 从请求体取
 
):
 
    return {
 
        "user_id": user_id,
 
        "user": user.model_dump(),
 
        "admin_note": admin_note,
 
        "priority": priority
 
    }
 

第二个接口的请求体长这样:

 
{
 
  "user": {"username": "张三", "email": "zhangsan@qq.com", "age": 25},
 
  "admin_note": "VIP 用户",
 
  "priority": 3
 
}
 

一句话总结:Field() 管”这个字段长什么样”,Body() 管”这个参数从哪来”。 大多数时候你只需要 Field(),只有在函数参数层面需要解决歧义时才用 Body()


三、完整示例:三种参数的综合运用

 
from fastapi import FastAPI, Query, Path, HTTPException
 
from pydantic import BaseModel, Field
 
from typing import Optional
 
app = FastAPI()
 
# ========== 模拟数据 ==========
 
fake_items = {
 
    1: {"id": 1, "name": "键盘", "price": 299.0, "tags": ["电子", "外设"]},
 
    2: {"id": 2, "name": "鼠标", "price": 149.0, "tags": ["电子", "外设"]},
 
    3: {"id": 3, "name": "显示器", "price": 1999.0, "tags": ["电子"]},
 
}
 
# ========== 请求体模型 ==========
 
class ItemCreate(BaseModel):
 
    name: str = Field(..., min_length=1, max_length=50, description="商品名称")
 
    price: float = Field(..., gt=0, description="商品价格,必须大于 0")
 
    tags: list[str] = Field(default=[], description="商品标签")
 
# 查询商品列表(查询参数)
 
@app.get("/items")
 
async def list_items(
 
    keyword: Optional[str] = Query(None, min_length=1, description="搜索关键词"),
 
    min_price: float = Query(0, ge=0, description="最低价格"),
 
    max_price: Optional[float] = Query(None, gt=0, description="最高价格"),
 
    skip: int = Query(0, ge=0, description="跳过多少条"),
 
    limit: int = Query(10, ge=1, le=100, description="返回多少条"),
 
):
 
    """用查询参数实现搜索和分页"""
 
    results = list(fake_items.values())
 
    # 按关键词过滤
 
    if keyword:
 
        results = [item for item in results if keyword in item["name"]]
 
    # 按价格过滤
 
    results = [item for item in results if item["price"] >= min_price]
 
    if max_price is not None:
 
        results = [item for item in results if item["price"] <= max_price]
 
    # 分页
 
    total = len(results)
 
    results = results[skip : skip + limit]
 
    return {"total": total, "items": results}
 
# 查询单个商品(路径参数)
 
@app.get("/items/{item_id}")
 
async def get_item(
 
    item_id: int = Path(..., gt=0, description="商品 ID")
 
):
 
    """用路径参数指定要查哪个商品"""
 
    if item_id not in fake_items:
 
        raise HTTPException(status_code=404, detail=f"商品 {item_id} 不存在")
 
    return fake_items[item_id]
 
# 创建商品(请求体)
 
@app.post("/items", status_code=201)
 
async def create_item(item: ItemCreate):
 
    """用请求体接收商品数据"""
 
    new_id = max(fake_items.keys()) + 1
 
    new_item = {"id": new_id, **item.model_dump()}
 
    fake_items[new_id] = new_item
 
    return new_item
 
# 综合运用:为指定用户创建订单(路径 + 查询 + 请求体)
 
@app.post("/users/{user_id}/orders")
 
async def create_order(
 
    user_id: int = Path(..., gt=0, description="用户 ID"),
 
    item: ItemCreate = ...,                              # 请求体
 
    quantity: int = Query(1, ge=1, le=99, description="购买数量"),
 
    note: Optional[str] = Query(None, description="订单备注"),
 
):
 
    """
 
    三种参数同时使用:
 
    - user_id:路径参数(指定给谁创建订单)
 
    - item:请求体(买了什么)
 
    - quantity、note:查询参数(额外选项)
 
    """
 
    total_price = item.price * quantity
 
    return {
 
        "user_id": user_id,
 
        "item": item.name,
 
        "quantity": quantity,
 
        "total_price": total_price,
 
        "note": note,
 
        "msg": f"用户 {user_id} 购买了 {quantity}{item.name},总价 {total_price} 元"
 
    }
 

测试这个综合接口:

 
POST /users/42/orders?quantity=3&note=尽快发货
 
Body: {"name": "键盘", "price": 299.0, "tags": ["电子"]}
 

返回:

 
{
 
  "user_id": 42,
 
  "item": "键盘",
 
  "quantity": 3,
 
  "total_price": 897.0,
 
  "note": "尽快发货",
 
  "msg": "用户 42 购买了 3 个键盘,总价 897 元"
 
}
 

速查表

概念一句话解释关键代码
请求体POST/PUT 发送的 JSON 数据def f(user: UserCreate)
BaseModel定义请求体结构class User(BaseModel): ...
Field()给请求体字段加约束(长度、范围)name: str = Field(..., min_length=3)
Query()给查询参数加约束q: str = Query(..., min_length=3)
Path()给路径参数加约束user_id: int = Path(..., gt=0)
Body()显式声明请求体字段info: str = Body(...)

▶ 对应原理:03-Pydantic数据校验与v2新特性

速记卡(面试闪卡)

Q1:一句话讲清「请求体与Pydantic」到底是什么?

A:用 Pydantic 模型当函数参数,FastAPI 自动把 JSON 请求体解析、校验成 Python 对象,省去手写解析。

Q2:一、请求体 Request Body —— 怎么理解? —— 怎么理解?

A:路径/查询参数塞在 URL 里,复杂或敏感的数据(用户名、邮箱)就放 HTTP body 里传 JSON。好比寄快递——地址写面单(URL),物件塞箱子里(body)。FastAPI 靠参数类型判断来源:BaseModel 子类自动当请求体。

Q3:二、Query / Path / Body / Field 显式声明 —— 怎么理解? —— 怎么理解?

A:当自动判断不够精确,用 Query()/Path()/Body() 给参数加约束(必填、长度、范围)。就像快递单填”重量 1-5kg、保价≤1000”。Field() 写在模型里约束字段本身(min_length、gt),Body() 用在函数参数上解决歧义。

Q4:三、FastAPI 怎么判断参数从哪来 —— 怎么理解? —— 怎么理解?

A:判断规则像查表:参数名在路径 {xxx} → 路径参数;类型是 BaseModel 子类 → 请求体;其他 → 查询参数。一张表定生死,调用方写 user: UserCreate 就自动从 body 取 JSON,返回 422 还会贴心地列出哪个字段不合法。

Q5:四、Field vs Body 的区别 —— 怎么理解? —— 怎么理解?

A:Field() 管”这个字段长什么样”(长度、范围、描述),写在模型里,用到哪都生效;Body() 管”这个参数从哪来”,用在函数参数上,解决多模型混用时的歧义。一句话:Field 守字段,Body 守来源。

Q6:核心速记主线有哪些?

  • 请求体放 JSON:存复杂/敏感数据,不在 URL 里

  • Pydantic 模型当参数:FastAPI 自动解析校验

  • Query/Path/Body/Field:显式加约束(必填、长度、范围)

  • 参数来源判定:路径参数 / 请求体 / 查询参数 三选一

口诀

A:请求体把 JSON 装,

Pydantic 把关强;

Field 管字段 Body 管向,

三类来源认得详。

相关链接


技术学习路线图 > 基础篇

相关链接