上一篇我们让接口从 URL 里拿到了参数。
这一篇换一个更常见的场景:前端提交一段 JSON,比如商品名称、价格、是否促销,FastAPI 怎么把这段 JSON 交给 Python 函数。
做完后,我们会在 /docs 里调用 POST /items。传对数据时接口正常返回,故意把价格传错时,FastAPI 会告诉我们错误出在请求体里的 price 字段。
我会继续将代码放到 fastapi-beginner-lab 仓库中,有需要的朋友自取。
这次要加什么
这篇仍然只改 app/main.py。
前两篇已经写过的 FastAPI()、GET /、GET /ping 和 GET /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响应。
下一篇继续往前走:接口返回值也不要随手拼,我们会用响应模型控制哪些字段能返回给前端。