FastAPI 新手入门第 3 篇:用 Pydantic 接收 JSON 请求体

上一篇我们让接口从 URL 里拿到了参数。

这一篇换一个更常见的场景:前端提交一段 JSON,比如商品名称、价格、是否促销,FastAPI 怎么把这段 JSON 交给 Python 函数。

做完后,我们会在 /docs 里调用 POST /items。传对数据时接口正常返回,故意把价格传错时,FastAPI 会告诉我们错误出在请求体里的 price 字段。

我会继续将代码放到 fastapi-beginner-lab 仓库中,有需要的朋友自取。

这次要加什么

这篇仍然只改 app/main.py

前两篇已经写过的 FastAPI()GET /GET /pingGET /items/{item_id} 这里不重复贴。我们只需要在已有代码上增加下面几行。

这段代码重点看两处:ItemCreate 描述请求体里应该有哪些字段,create_item(item: ItemCreate) 让 FastAPI 知道这个接口要从 JSON 请求体里读取数据。

python 复制代码
from pydantic import BaseModel


class ItemCreate(BaseModel):
    name: str
    price: float
    is_offer: bool = False


@app.post("/items")
def create_item(item: ItemCreate):
    return item.model_dump()

这里多了一个 ItemCreate。它不是数据库表,也不是最终返回给前端的完整商品对象。现在只把它当成"创建商品时,前端需要提交哪些数据"的说明。

/docs 里提交 JSON

启动服务的方式和前两篇一样:

powershell 复制代码
.\.venv\Scripts\Activate.ps1
$env:PYTHONIOENCODING = "utf-8"
$env:PYTHONUTF8 = "1"
fastapi dev app/main.py

打开:

text 复制代码
http://127.0.0.1:8000/docs

这次点开 POST /items,再点 Try it out。页面会给出一个请求体输入框。

填入这段 JSON:

json 复制代码
{
  "name": "Notebook",
  "price": 12.5,
  "is_offer": true
}

Execute 后,应该能看到响应:

json 复制代码
{
  "name": "Notebook",
  "price": 12.5,
  "is_offer": true
}

到这里,URL 里没有商品名称,也没有价格。数据是放在请求体里的。

请求体就是客户端随请求一起发过来的数据。创建、修改资源时,JSON 通常就放在这里。

为什么不用 dict 随手接

如果我们把接口写成这样:

python 复制代码
@app.post("/items")
def create_item(item: dict):
    return item

这段代码也能收到 JSON,但问题会很快出现:item 里有什么字段、字段是什么类型、哪些字段必须传,都只能靠人记。

写成 ItemCreate 后,这些规则直接放进代码里:

python 复制代码
class ItemCreate(BaseModel):
    name: str
    price: float
    is_offer: bool = False

这三行表达了三个约定:

  • name: str:必须传商品名称,而且应该是字符串。
  • price: float:必须传价格,而且应该能变成小数。
  • is_offer: bool = False:可以不传,不传时默认是 False

这就是 Pydantic 模型在这里做的事:把一段松散的 JSON,变成有字段、有类型、有默认值的 Python 对象。

故意传错一次

现在在 /docs 里把请求体改成这样:

json 复制代码
{
  "name": "Notebook",
  "price": "cheap",
  "is_offer": true
}

price 这里故意传了 "cheap"

再次点 Execute,FastAPI 会返回 422。响应里会有类似这样的信息:

json 复制代码
{
  "loc": ["body", "price"],
  "msg": "Input should be a valid number"
}

这条错误信息够用了。

body 表示错误来自请求体,price 表示出错字段是价格。我们不用进入函数里手写一堆 if,FastAPI 已经在函数执行前把不合适的数据挡住了。

函数里拿到的是什么

create_item 里的 item 不是普通 dict,而是 ItemCreate 对象。

所以在函数里可以这样访问字段:

python 复制代码
@app.post("/items")
def create_item(item: ItemCreate):
    return {
        "name": item.name,
        "price": item.price,
        "is_offer": item.is_offer,
    }

示例项目里我用了另一种写法:

python 复制代码
@app.post("/items")
def create_item(item: ItemCreate):
    return item.model_dump()

model_dump() 会把 Pydantic 模型转回普通字典。现在我们只是把收到的数据原样返回,所以这样写更短。

等后面接数据库时,我们会在这里做更多事,比如生成 ID、保存数据、返回创建后的结果。

FastAPI 在这里做了什么

从浏览器点 Execute 到函数拿到 item,中间发生了几件事:

text 复制代码
读取请求体 JSON -> 按 ItemCreate 检查字段 -> 转成 Python 对象 -> 执行 create_item

如果 JSON 不符合 ItemCreate,流程会停在检查字段这一步,FastAPI 返回 422

如果 JSON 合法,create_item 才会被执行。

这也是我不想一开始就用 dict 的原因。新手写后端时,最容易混乱的地方不是"怎么拿到 JSON",而是"拿到之后怎么确认它能用"。

ItemCreate 把这件事提前说清楚了。

动手改一下

现在给 ItemCreate 增加一个库存字段:

python 复制代码
class ItemCreate(BaseModel):
    name: str
    price: float
    is_offer: bool = False
    stock: int

保存后回到 /docs,用这段请求体试一下:

json 复制代码
{
  "name": "Notebook",
  "price": 12.5,
  "is_offer": true,
  "stock": "many"
}

如果返回 422,并且错误位置指向:

json 复制代码
["body", "stock"]

说明你已经把请求体模型和字段校验跑通了。

到这里,这篇的目标已经完成:

  • 我们用 ItemCreate 描述了创建商品时需要的 JSON。
  • 我们跑通了 POST /items
  • 我们在 /docs 里看到了字段类型错误对应的 422 响应。

下一篇继续往前走:接口返回值也不要随手拼,我们会用响应模型控制哪些字段能返回给前端。