为什么使用 FastAPI 上传图像时会收到“无法处理的实体”错误?

dia*_*sis 5 python python-3.x fastapi

我正在尝试上传图像,但 FastAPI 返回时出现一个我无法弄清楚的错误。

file: UploadFile = File(...)如果我从函数定义中省略“ ”,它就会正常工作。但是当我将 the 添加file到函数定义中时,它会抛出错误。

这是完整的代码。

@router.post('/', response_model=schemas.PostItem, status_code=status.HTTP_201_CREATED)
def create(request: schemas.Item, file: UploadFile = File(...), db: Session = Depends(get_db)):

    new_item = models.Item(
        name=request.name,
        price=request.price,
        user_id=1,
    )
    print(file.filename)
    db.add(new_item)
    db.commit()
    db.refresh(new_item)
    return new_item
Run Code Online (Sandbox Code Playgroud)

Pydantic模型Item只是

class Item(BaseModel):
    name: str
    price: float
Run Code Online (Sandbox Code Playgroud)

错误是:
代码 422 错误:无法处理的实体

{
  "detail": [
    {
      "loc": [
        "body",
        "request",
        "name"
      ],
      "msg": "field required",
      "type": "value_error.missing"
    },
    {
      "loc": [
        "body",
        "request",
        "price"
      ],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

Gin*_*pin 5

问题是您的路由需要两种类型的请求正文:

  • request: schemas.Item

    • 这正在等待发布application/json正文
    • 请参阅FastAPI 文档的请求正文部分:“以 JSON 形式读取请求正文
  • file: UploadFile = File(...)

    • 这正在等待发布multipart/form-data
    • 请参阅FastAPI 文档的请求文件部分:“ FastAPI将确保从正确的位置而不是 JSON读取数据。...当表单包含文件时,它会被编码为multipart/form-data

这是行不通的,因为这不仅破坏了 FastAPI,还破坏了通用的 HTTP 协议。FastAPI 在使用时的警告File中提到了这一点:

您可以在路径操作中声明多个Fileand参数,但也不能声明您希望接收为 JSON 的字段,因为请求将使用而不是使用.FormBodymultipart/form-dataapplication/json

这不是FastAPI的限制,它是 HTTP 协议的一部分。

正如将文件和关联数据发布到 RESTful WebService(最好以 JSON 形式)中所述,常见的解决方案是:

  1. 将 API 分为 2 个 POST 请求:1 个用于文件,1 个用于元数据
  2. 全部发送到 1multipart/form-data

幸运的是,FastAPI 支持解决方案 2,将您的模型组合起来Item并将文件上传到 1 中multipart/form-data请参阅请求表格和文件部分:

当您需要在同一请求中接收数据和文件时,请一起使用File和。Form

这是您修改后的路线(我删除了,db因为这与问题无关):

class Item(BaseModel):
    name: str
    price: float

class PostItem(BaseModel):
    name: str

@router.post('/', response_model=PostItem, status_code=status.HTTP_201_CREATED)
def create(
    # Here we expect parameters for each field of the model
    name: str = Form(...),
    price: float = Form(...),
    # Here we expect an uploaded file
    file: UploadFile = File(...),
):
    new_item = Item(name=name, price=price)
    print(new_item)
    print(file.filename)
    return new_item
Run Code Online (Sandbox Code Playgroud)

Swagger 文档将其呈现为 1 种形式

带表单的 swagger UI

...您现在应该能够Item在一个请求中发送参数和文件。

如果您不喜欢将Item模型拆分为单独的参数(对于具有许多字段的模型来说确实很烦人),请参阅此 Q&A on fastapi form data with pydantic model

下面是修改后的代码,其中Item更改为ItemForm支持接受其字段作为Form值而不是 JSON:

class ItemForm(BaseModel):
    name: str
    price: float

    @classmethod
    def as_form(cls, name: str = Form(...), price: float = Form(...)) -> 'ItemForm':
        return cls(name=name, price=price)

class PostItem(BaseModel):
    name: str

@router.post('/', response_model=PostItem, status_code=status.HTTP_201_CREATED)
def create(
    item: ItemForm = Depends(ItemForm.as_form),
    file: UploadFile = File(...),
):
    new_item = Item(name=item.name, price=item.price)
    print(new_item)
    print(file.filename)
    return new_item
Run Code Online (Sandbox Code Playgroud)

Swagger UI 应该仍然相同(所有字段Item和文件上传都在一种表单中)。

为了这:

file: UploadFile = File(...)如果我从函数定义中省略“ ”,它就会正常工作

关注这一点并不重要,但它确实有效,因为删除File会将预期的请求正文恢复为application/json类型,因此 JSON 正文将起作用。

最后,作为旁注,我强烈建议不要用作request路线的参数名称。除了含糊不清(一切都是请求)之外,直接使用Request对象request: Request时可能会与FastAPI的参数发生冲突。