2

I'm working on a website where the frontend is done in React and the backend in Python with FastAPI. I made a form which takes a some data and sends it to the backend with axios. It looks like this

{
name='Jonathan',
aliases=["Johnny"],
birthdate='2-15-1980',
gender='male', 
height=178 
weight=90 
nationalities=["American", "French"], 
occupations=["Programmer", "Comedian"], 
status='single', 
images=[
  {'attachment': FileList,
   'location': 'Berlin',
   'date': '10-14-2019'
  }
]
}

However, when I submit it, FastAPI seems to remove the images from the form.

name='Jonathan',
aliases=["Johnny"],
birthdate='2-15-1980',
gender='male', 
height=178 
weight=90 
nationalities=["American", "French"], 
occupations=["Programmer", "Comedian"], 
status='single', 
images=[
{'attachment': {'0': {}}, 'location': 'Berlin', 'date': '10-14-2019'}
]

This is what the route currently looks like

@router.post("/register/user")
def register_user(user_data: UserCreate):
    print(user_data)

I'm not entirely sure what's going on. I'm guessing it has something to do with how the data is send and its encryption. I'm at a dead end here. Thanks in advance.

Edit: This is what the UserCreate Schema looks like

class CharacterCreate(BaseModel):
    name: str
    aliases: list

    birthdate: Optional[str]
    gender: str
    height: Optional[float]
    weight: Optional[float]

    nationalities: Optional[set[str]]
    occupations: Optional[set[str]]

    status: str
    images: Optional[list]
Chris
  • 18,724
  • 6
  • 46
  • 80
Maypher
  • 90
  • 1
  • 11
  • Have you checked in your browser's development tools (under Network) what actually gets submitted to FastAPI when you reference `FileList` in your frontend code? I'm guessing what you see is what you actually submit, and that `FileList` isn't serializable in the way you expect. – MatsLindh Mar 05 '22 at 12:39
  • @Chris Edited the question with the model – Maypher Mar 05 '22 at 13:29
  • @MatsLindh To clarify, `FileList` seems to be a buit-in JS object, not a custom object. In the request payload it only shows as `images=[object Object]` – Maypher Mar 05 '22 at 13:31
  • @Chris yes. These images should also contain some information about them – Maypher Mar 05 '22 at 13:40
  • @Chris I saw that post but it looks like to use `Form(...)` I need to accept each field individually and that would make the function have lots of parameters. Is there any way to accept the files as one parameter and the rest of the form as another? – Maypher Mar 05 '22 at 14:08

1 Answers1

0

As per the documentation, you can't have both JSON and Files / form-data in the same request, as the body is encoded using multipart/form-data (when files are included) or application/x-www-form-urlencoded (if only Form data included) instead of application/json. Have a look at this answer.

Thus, one way to solve this is to use a single parameter to hold the various "metadata" and have a second one for the files/images. Using Dependency Injection you can check the received data against the Pydantic model using parse_raw method, before proceeding to the endpoint. If a ValidationError is thrown, then an HTTPException (HTTP_422_UNPROCESSABLE_ENTITY) should be raised, including the errors. Example below. Alternatively, have a look at Method 4 in this answer.

app.py

from fastapi import FastAPI, Form, File, UploadFile, status
import pydantic
from pydantic import BaseModel
from typing import Optional, List
from fastapi.exceptions import HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi import Depends

app = FastAPI()

class User(BaseModel):
    name: str
    aliases: List[str]
    height: Optional[float]
    weight: Optional[float]

def checker(data: str = Form(...)):
    try:
        user = User.parse_raw(data)
    except pydantic.ValidationError as e:
        raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
    return user
    
@app.post("/submit")
def submit(user: User = Depends(checker), files: List[UploadFile] = File(...)):
        return {"User": user, "Uploaded Files": [file.filename for file in files]}

test.py

import requests

url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data' : '{"name": "foo", "aliases": ["alias_1", "alias_2"], "height": 1.80, "weight": 80.5}'}
resp = requests.post(url=url, data=data, files=files) 
print(resp.json())
Chris
  • 18,724
  • 6
  • 46
  • 80