5

I am trying to upload an image but FastAPI is coming back with an error I can't figure out.

If I leave out the "file: UploadFile = File(...)" from the function definition, it works correctly. But when I add the file to the function definition, then it throws the error.

Here is the complete code.

@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

The Item Pydantic model is just

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

The error is:
Code 422 Error: Unprocessable Entity

{
  "detail": [
    {
      "loc": [
        "body",
        "request",
        "name"
      ],
      "msg": "field required",
      "type": "value_error.missing"
    },
    {
      "loc": [
        "body",
        "request",
        "price"
      ],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}
Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
dianesis
  • 333
  • 4
  • 17
  • Please [edit] to post the _exact_ error message and response. Also, how are you sending the POST request? How are you uploading the file? – Gino Mempin Sep 23 '21 at 01:24
  • I am sending it using the SwaggerUI. So I click in the SwaggerUI file browser and select the image and upload. If I leave the request: ItemShow and the ItemShow from the route it works. SO I think I cannot have both the request: schemas.Item and file: UploadFile... – dianesis Sep 23 '21 at 01:30

1 Answers1

5

The problem is that your route is expecting 2 types of request body:

  • request: schemas.Item

    • This is expecting POSTing an application/json body
    • See the Request Body section of the FastAPI docs: "Read the body of the request as JSON"
  • file: UploadFile = File(...)

    • This is expecting POSTing a multipart/form-data
    • See the Request Files section of the FastAPI docs: "FastAPI will make sure to read that data from the right place instead of JSON. ...when the form includes files, it is encoded as multipart/form-data"

That will not work as that breaks not just FastAPI, but general HTTP protocols. FastAPI mentions this in a warning when using File:

You can declare multiple File and Form parameters in a path operation, but you can't also declare Body fields that you expect to receive as JSON, as the request will have the body encoded using multipart/form-data instead of application/json.

This is not a limitation of FastAPI, it's part of the HTTP protocol.

The common solutions, as discussed in Posting a File and Associated Data to a RESTful WebService preferably as JSON, is to either:

  1. Break the API into 2 POST requests: 1 for the file, 1 for the metadata
  2. Send it all in 1 multipart/form-data

Fortunately, FastAPI supports solution 2, combining both your Item model and uploading a file into 1 multipart/form-data. See the section on Request Forms and Files:

Use File and Form together when you need to receive data and files in the same request.

Here's your modified route (I removed db as that's irrelevant to the problem):

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

The Swagger docs present it as 1 form

swagger UI with form

...and you should be able now to send both Item params and the file in one request.

If you don't like splitting your Item model into separate parameters (it would indeed be annoying for models with many fields), see this Q&A on fastapi form data with pydantic model.

Here's the modified code where Item is changed to ItemForm to support accepting its fields as Form values instead of 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

The Swagger UI should still be the same (all the Item fields and the file upload all in one form).

For this:

If I leave out the "file: UploadFile = File(...)" from the function definition, it works correctly

It's not important to focus on this, but it worked because removing File turned the expected request body back to an application/json type, so the JSON body would work.

Finally, as a side note, I strongly suggest NOT using request as a parameter name for your route. Aside from being vague (everything is a request), it could conflict with FastAPI's request: Request parameter when using the Request object directly.

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
  • Well this is an incredibly instructive answer. By the way I read all the time about splitting the request into the body and the file into two different requests. This is strange as most of the web work I have done an image is typically part of a request along with other values like name price etc. – dianesis Sep 23 '21 at 14:30
  • I have one more comment. FastAPI is good but the error messages needs to be more expressive. I had encounter errors where I have no clues on what's going on. If you can throw some lights into how you interpret this error to reach the conclusion that I was doing all this wrong. In other words, to teach me/others your mental process to figure this out, I will appreciate it. – dianesis Sep 23 '21 at 14:33
  • Also, I noticed the price: str = Form(...), instead of float which is the right pydantic type. Is this an error? – dianesis Sep 23 '21 at 14:46
  • 1
    @dianesis Yes, the price was a typo from copy-pasting without checking. It should be float in all the sample codes. Updated my answer. – Gino Mempin Sep 23 '21 at 23:46
  • @dianesis [1/2] For the 422 fastapi/pydantic error, I regularly get it when I'm either _passing_ the request wrong or _receiving_ the request wrong. I started with trying 1 route with just `item: Item` and 1 route with just `file: UploadFile...`, just to confirm they work normally as-is. Then, when it didn't work together, I [accessed the `Request` object directly](https://fastapi.tiangolo.com/advanced/using-request-directly/) to see the actual content-type and body of the request. – Gino Mempin Sep 23 '21 at 23:55
  • 1
    @dianesis [2/2] I checked `request.headers['content-type']` and saw that it was `multipart/form-data`. That gave me a hint that it won't get parsed properly into the Pydantic model, which accepts a dict (so the body should be in JSON). Then I re-read the FastAPI docs on the uploading files, and saw that warning about mixing JSON and Form in 1 request. (In hindsight, re-reading the docs first would have solved the problem much more quickly.) – Gino Mempin Sep 24 '21 at 00:01