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]
3
  • 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. Commented Mar 5, 2022 at 12:39
  • @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] Commented Mar 5, 2022 at 13:31
  • @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? Commented Mar 5, 2022 at 14:08

1 Answer 1

0

Update

For more options, please have a look at this answer.

Original Answer

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 (Note: In Pydantic V2 parse_raw has been deprecated and replaced by model_validate_json), before proceeding to the endpoint. If a ValidationError is thrown, then an HTTPException (HTTP_422_UNPROCESSABLE_ENTITY) should be raised, including the errors.

Example

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.model_validate_json(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())
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.