如何在FastAPI中使用Pydantic的BaseModel上传文件和字典列表?
如何在FastAPI中使用Pydantic的BaseModel上传文件和字典列表?
问题:
我有以下代码示例:
from fastapi import File, UploadFile, Request, FastAPI, Depends
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI()
class BaseBox(BaseModel):
l: float=Field(...)
t: float=Field(...)
r: float=Field(...)
b: float=Field(...)
class BaseInput(BaseModel):
boxes: List[BaseBox] = Field(...)
words: List[str] = Field(...)
width: Optional[float] = Field(...)
height: Optional[float] = Field(...)
@app.post("/submit")
def submit(
base_input: BaseInput = Depends(),
file: UploadFile = File(...), # Add this line to accept a file
):
return {
"JSON Payload": base_input,
"Filename": file.filename,
}
@app.get("/")
def main(request: Request):
return {"status":"alive"}
但有些我怎么做不到。我使用交互式API文档,但总是出现错误。你认为我必须发送两个文件吗?我也试过
curl -X 'POST' \
'http://localhost:8007/submit?width=10&height=10' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F '[email protected];type=image/png' \
-F 'boxes={
"l": 0,
"t": 0,
"r": 0,
"b": 0
}' \
-F 'words=test,test2,tes3,test'
但我总是得到错误
"POST /submit?width=10&height=10 HTTP/1.1" 422 Unprocessable Entity。
回答:
正如您提供的代码所示,您已经了解了这个答案,这就是您最终应该找到的解决方案。
但是,让我解释一下您的示例中的代码有什么问题。您同时提交了文件和查询数据,或者,至少,这似乎是您一直在努力实现的。在端点或Pydantic模型
中定义查询参数,例如,定义为str
或int
,并在端点中的参数上使用Depends()
来指示在BaseModel
中定义的文件应作为查询参数,在这两种情况下,都应该可以正常工作。但是,当您直接在端点或在BaseModel
中将参数定义为List
、e.g
.、List[int]
或List[str]
时,您应该使用Query
显式定义它,如这里和这里所解释和演示的。
虽然Pydantic模型
过去不允许使用Query字段
,并且必须在单独的依赖类中实现查询parameter-parsing
,如本答案和本答案所示,但这一点最近发生了变化,因此,可以使用BaseModel类
将Query()
封装在Field()
中,如本回答所示。
工作示例1
from fastapi import Query
from pydantic import BaseModel, Field
from typing import Optional
class Base(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
@app.get('/')
async def main(base: Base = Depends()):
pass
在您的示例中,您似乎还有一个List查询参数,该参数需要一个dictionary/JSON对象列表
作为值。但是,使用查询参数无法实现这一点。如果您试图在上面的工作示例中定义这样的参数(e.g.,boxes: List[BaseBox] = Field (Query(...)))
,则在尝试运行FastAPI应用程序
时会遇到以下错误:
断言错误:参数:boxes只能是请求体,使用Body()
如果您将参数定义为boxes: List[BaseBox] = Field (...)
,以及在端点中定义的file: UploadFile = File(...)
,就像您在代码中已经做的那样,即使应用程序会像往常一样开始运行,但当尝试向该端点提交请求时(例如,通过位于/docs的Swagger UI autodocs
),您会收到一个422 Unprocessable Entity错误
,并显示一条具有类似含义的消息,说Input should be a valid dictionary or object to extract fields from
。
这是因为,在第一种情况下,您不能有一个期望字典数据的查询参数(除非您对任意查询数据遵循此处和此处描述的方法,您需要自己解析这些数据,我不建议这样做),而在第二种情况中,由于端点中定义的file: UploadFile = File(...)
,请求体被编码为multipart/form-data
发送;但是,HTTP协议
不支持同时发送Form
和JSON数据
(请再次参阅此答案)。
但是,如果您从端点中删除了UploadFile参数
,那么请求应该会成功通过,因为请求体将被内插为application/json
(提交请求时,请查看Swagger UI
中的Content-Type请求头
)。
工作示例2
from fastapi import Query
from pydantic import BaseModel, Field
from typing import Optional
class BaseBox(BaseModel):
l: float = Field(...)
t: float = Field(...)
class Base(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
boxes: List[BaseBox] = Field (...)
@app.get('/')
async def main(base: Base = Depends()):
pass
发布文件和JSON正文(包括字典的List)
如果您仍然需要在FastAPI POST请求
中同时添加File(s)
和JSON主体
,我强烈建议您看看这个答案的方法3和4。下面提供的示例基于链接答案中的这两种方法,并演示了如何将Files
与JSON数据
一起发布,JSON数据
还包括字典列表,就像您的示例一样。请查看链接的答案,了解有关如何测试这些方法的更多详细信息和Python
和JavaScript
中的示例。在您的情况下,您需要将查询参数与正文字段分开,并在端点(如前面提供的链接答案中所述)或单独的Pydantic模型
中定义查询参数,如下所示。
工作示例3(基于此答案的方法3)
在Swagger UI /docs
中,由于data
是一个Form参数
并表示为单个字段,因此您需要将该字段中Base
的数据作为字典传递,该字典将作为str
提交到data Form参数
中。测试示例:
{"boxes": [{"l": 0,"t": 0,"r": 0,"b": 0}], "comments": ["foo", "bar"], "code": 0}
有关如何测试的更多信息,请参阅上面的链接答案。
app.py
from fastapi import FastAPI, status, Form, UploadFile, File, Depends, Query
from pydantic import BaseModel, Field, ValidationError
from fastapi.exceptions import HTTPException
from fastapi.encoders import jsonable_encoder
from typing import Optional, List
app = FastAPI()
class BaseParams(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
class BaseBox(BaseModel):
l: float=Field(...)
t: float=Field(...)
r: float=Field(...)
b: float=Field(...)
class Base(BaseModel):
boxes: List[BaseBox] = Field (...)
comments: List[str] = Field (...)
code: int = Field (...)
def checker(data: str = Form(...)):
try:
return Base.model_validate_json(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
@app.post("/submit")
def submit(
base_params: BaseParams = Depends(),
base: Base = Depends(checker),
files: List[UploadFile] = File(...),
):
return {
"Params": base_params,
"JSON Payload": base,
"Filenames": [file.filename for file in files],
}
工作示例4(基于此答案的方法4)
这种方法的优点是需要更少的代码来实现预期的结果,而且Base模型
在Swagger UI /docs的请求
主体部分中表示(使用自动生成的输入示例),从而使数据视图更清晰,发布数据的方式更容易。同样,请查看上面的链接答案,了解有关此方法的更多详细信息。
app.py
from fastapi import FastAPI, Body, UploadFile, File, Depends, Query
from pydantic import BaseModel, Field, model_validator
from typing import Optional, List
import json
app = FastAPI()
class BaseParams(BaseModel):
width: Optional[float] = Field (...)
height: Optional[float] = Field (...)
words: List[str] = Field (Query(...)) # wrap the `Query()` in a `Field()` for `List` params
class BaseBox(BaseModel):
l: float=Field(...)
t: float=Field(...)
r: float=Field(...)
b: float=Field(...)
class Base(BaseModel):
boxes: List[BaseBox] = Field (...)
comments: List[str] = Field (...)
code: int = Field (...)
@model_validator(mode="before")
@classmethod
def validate_to_json(cls, value):
if isinstance(value, str):
return cls(**json.loads(value))
return value
@app.post("/submit")
def submit(
base_params: BaseParams = Depends(),
base: Base = Body(...),
files: List[UploadFile] = File(...),
):
return {
"Params": base_params,
"JSON Payload": base,
"Filenames": [file.filename for file in files],
}