Fastapi: multipart/form-data: Object parsing error when using files and object in request form

Created on 17 Nov 2020  路  5Comments  路  Source: tiangolo/fastapi

First check

  • [x] I added a very descriptive title to this issue.
  • [x] I used the GitHub search to find a similar issue and didn't find it.
  • [x] I searched the FastAPI documentation, with the integrated search.
  • [x] I already searched in Google "How to X in FastAPI" and didn't find any information.
  • [x] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [x] I already checked if it is not related to FastAPI but to Pydantic.
  • [x] I already checked if it is not related to FastAPI but to Swagger UI.
  • [x] I already checked if it is not related to FastAPI but to ReDoc.
  • [x] After submitting this, I commit to one of:

    • Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.

    • I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.

    • Implement a Pull Request for a confirmed bug.

Example

Here's a self-contained, minimal, reproducible, example with my use case:

from fastapi import FastAPI, File, Form
from pydantic import BaseModel

app = FastAPI()


class Address(BaseModel):
    first_line: str
    post_code: str


class User(BaseModel):
    first_name: str
    last_name: str
    address: Address


@app.post("/users/")
async def create_user(avatar: bytes = File(...), user: User = Form(...)):
    return {
        "file_size": len(avatar),
        "user": user,
    }


if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app, host='0.0.0.0', port=8000, debug=True)

Description

  • Open the browser and call the endpoint /docs. Click try it out on the API
  • upload a file, click execute
  • It returns error with status 422 Unprocessable Entity
{
  "detail": [
    {
      "loc": [
        "body",
        "user"
      ],
      "msg": "value is not a valid dict",
      "type": "type_error.dict"
    }
  ]

.

Environment

  • OS: "Ubuntu 18.04.5 LTS"
  • pip
    fastapi==0.61.2
    python-multipart==0.0.5
    requests==2.24.0
    starlette==0.13.6
    uvicorn==0.12.1
    ...

  • Python version: 3.8.6

Additional context

I found #842 has similar issue, this one is more a mixed use of files and object in a form data.

Not sure if I messed something up, but thanks for any advice.

question

All 5 comments

The problem is using a pydantic model with form.
I recommend what I call Mause Form Solution: https://github.com/tiangolo/fastapi/issues/1989#issuecomment-684170738

Thanks @Kludex for pointing out Mause's solution. That is a really nice solution. I will close this issue.

Hopping in this as I have a slightly different use (but essentially the same issue)

I've been using a solution similar to Mause's solution. I use an as_form decorator to add a method rather than converting the class. Both these workarounds fall short of working for nested dictionaries.

from typing import Dict

from fastapi import FastAPI, Depends, Form
from pydantic import BaseModel

app = FastAPI()

@as_form
class Item(BaseModel):
    name: str
    another: str
    opts: Dict[str, int] = {}

@app.post('/test', response_model=Item)
def endpoint(item: Item = Depends(Item.as_form)):
    return item

Both styles of docs show the opts field as a dict, with the redoc style even showing that property name is an integer

I don't believe my method would work for nested fields I'm afraid.

Perhaps you could provide a sample of the form body you are posting and the source of your as_form function in a new issue?

This works for me, here is a full as is example: Surprisingly it works with example as well. Worth mentioning that I have a slightly different signature of the view function.

from typing import List

from fastapi import FastAPI, File, Form
from pydantic import BaseModel

app = FastAPI()


def as_form(cls):
    cls.__signature__ = cls.__signature__.replace(
        parameters=[arg.replace(default=Form(...)) for arg in cls.__signature__.parameters.values()]
    )
    return cls


class Address(BaseModel):
    first_line: str
    postcode: str


@as_form
class User(BaseModel):
    first_name: str
    last_name: str
    address: List[Address]

    class Config:
        schema_extra = {
            'example': {
                'first_name': 'John',
                'last_name': 'Lewis',
                'address': [
                    {'first_line': '5', 'postcode': '123456'},
                    {'first_line': '6', 'postcode': '123456'},
                ],
            }
        }


@app.post("/users/")
async def create_user(avatar: bytes = File(None), user: User = Form(...)):
    return {
        "file_size": avatar and len(avatar),
        "user": user,
    }


if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app, host='0.0.0.0', port=8001, debug=True)

Was this page helpful?
0 / 5 - 0 ratings