I am following Method 2 of this answer to be able to upload multiple files in combination with additional data using fastapi. It is working fine.
After starting to implement the handling of the additional data including validation using pydantic's BaseModel
i am facing an issue:
My custom field_validator
is working when using the model class directly but it is not working as expected, when using it via FastAPI and Depends()
.
The key point is that i want to use a python Enum
and i want to be able to use the Enum's names
in the additional data (query parameters). For this reason i am using a custom validator to only allow names which exist in the enum.
When i initialize the model manually, the validation works as expected:
from enum import Enumfrom pydantic import BaseModel, field_validator, ValidationInfo, ValidationErrorclass VehicleSubSystems(Enum): A = "A Verbose" B = "B Verbose"class EvaluationArguments(BaseModel): vehicle_sub_system: VehicleSubSystems @field_validator("vehicle_sub_system", mode='before') @classmethod def validate_vehicle_sub_system(cls, vehicle_sub_system: str, _info: ValidationInfo) -> VehicleSubSystems:""" Allows using the enum names instead of the values """ try: return VehicleSubSystems[vehicle_sub_system] except KeyError: raise ValueError(f"Can not find vehicle subsystem '{vehicle_sub_system}'. " f"Allowed values: {[e.name for e in VehicleSubSystems]}")def test_validation_is_performed():""" Test that the validation is performed """ EvaluationArguments(vehicle_sub_system="A") try: EvaluationArguments(vehicle_sub_system="DOES_NOT_EXIST") except ValidationError: print("Test passed") else: print("Test failed")if __name__ == '__main__': test_validation_is_performed() # prints "Test passed" as expected
Combining this with the FastAPI application shows unexpected behavior: The field_validator is not considered. Instead the default behavior of the model class is used.
Server code:
import uvicornfrom typing import Listfrom fastapi import FastAPI, File, Depends, UploadFileapp = FastAPI()def create_args(vehicle_sub_system: str): return EvaluationArguments(vehicle_sub_system=vehicle_sub_system)@app.post("/process-works")def process_works(files: List[UploadFile] = File(...), eval_args: EvaluationArguments = Depends(create_args)): return f"Got {len(files)} files and {eval_args}"@app.post("/process-fails")def process_fails(files: List[UploadFile] = File(...), eval_args: EvaluationArguments = Depends()): return f"Got {len(files)} files and {eval_args}"if __name__ == '__main__': uvicorn.run(app, host="0.0.0.0", port=8000)
Client code:
import requestsif __name__ == '__main__': url = 'http://127.0.0.1:8000' files = [('files', open('d:/temp/a.txt', 'rb')), ('files', open('d:/temp/b.txt', 'rb'))] params = {"vehicle_sub_system": "A"} print("Calling process-works") resp = requests.post(url=f"{url}/process-works", params=params, files=files) print(resp.json()) print("Calling process-fails") resp = requests.post(url=f"{url}/process-fails", params=params, files=files) print(resp.json()) # Output # Calling process-works # Got 2 files and vehicle_sub_system=<VehicleSubSystems.A: 'A Verbose'> # Calling process-fails # {'detail': [{'type': 'enum', 'loc': ['query', 'vehicle_sub_system'], 'msg': "Input should be 'A Verbose' or 'B Verbose'", 'input': 'A', 'ctx': {'expected': "'A Verbose' or 'B Verbose'"}}]}
The process-works
endpoint shows the expected behavior but only when using a separate dependency Depends(create_args)
which mimics the direct usage of the model class.
The process-fails
endpoint (using Depends()
) shows the issue. I would expect that Depends()
is just making FastAPI to call the init method of the model class and uses the validation as expected. But somehow it just ignores it.
I could not figure out why, perhaps somebody can explain what happens here and if there is a solution without the workaround?