0
import uvicorn
from fastapi import FastAPI, Depends
from pydantic import BaseModel, root_validator

app = FastAPI(docs_url=f"/docs")

class MyClass(BaseModel):
    my_string_field: str

    @root_validator
    def anything_not_named_just_valiadate(cls, values):
        my_string_field = values.get('my_string_field')
        if my_string_field != 'good':
            raise ValueError('not good')
        return values

@app.post("/myendpoint")
def scope(variable: MyClass = Depends()):
    return 'ok'


uvicorn.run(app, host="0.0.0.0", port=8000)

curl -X POST http://localhost:8000/myendpoint?my_string_field=good

passes with 200 as expected, however

curl -X POST http://localhost:8000/myendpoint?my_string_field=bad

results in 500 Internal Server Error, but I would expect 422 Unprocessable Entity from model validation error. How to get 422?

PS. assert instead of raise ValueError is the same

mrc
  • 126
  • 1
  • 11
  • Ok it seems FastAPI requires an exception handler for that, see https://stackoverflow.com/a/68916922/1845207 For me that is weird because if a model requires an int but I give it a string then 422 is automatic. Another option is raising HTTPException(422, 'reason') instead of ValueError but then the 'reason' should best follow schema, for which I think it's easier to just RequestValidationError(errors=[ErrorWrapper(ValueError... like in the workaround below. – mrc Mar 03 '23 at 13:48
  • And official FastAPI documentation actually explains why 500 in the bottom of this section: https://fastapi.tiangolo.com/uk/tutorial/handling-errors/#override-request-validation-exceptions: "It should be this way because if you have a Pydantic ValidationError in your response or anywhere in your code (not in the client's request), it's actually a bug in your code.". Then the question is how could I raise a validation error in the request validation. – mrc Mar 04 '23 at 09:29

1 Answers1

0

This is a workaround, but I could write a custom init and raise RequestValidationError but isn't it ugly?

import uvicorn
from fastapi import FastAPI, Depends
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel
from pydantic.error_wrappers import ErrorWrapper

app = FastAPI(docs_url=f"/docs")


class MyClass(BaseModel):
    my_string_field: str

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.my_string_field != 'good':
            raise RequestValidationError(errors=[ErrorWrapper(ValueError(f'Error expected "good"'), loc=('body', 'my_string_field'))])


@app.post("/myendpoint")
def scope(variable: MyClass = Depends()):
    return 'ok'


uvicorn.run(app, host="0.0.0.0", port=8000)

A different approach is creating a subclass (extending str in this case) and implementing custom validators similarly as in How to include non-pydantic classes in fastapi responses? and https://github.com/tiangolo/sqlmodel/issues/235#issuecomment-1162063590

import uvicorn
from pydantic import BaseModel
from fastapi import FastAPI, Depends
from typing import Type, Iterable, Callable, Any

app = FastAPI(docs_url=f"/docs")


class CustomString(str):
    @classmethod
    def __get_validators__(cls: Type["CustomString"]) -> Iterable[Callable[..., Any]]:
        yield cls.validate

    @classmethod
    def validate(cls, v: str) -> Any:
        if v != "good":
            raise ValueError(f"Expected good, received: {v}")
        return v

class MyClass(BaseModel):
    my_string_field: CustomString

@app.post("/myendpoint")
def scope(variable: MyClass = Depends()):
    return 'ok'


uvicorn.run(app, host="0.0.0.0", port=8000)

This works, 422 is generated "automatically".

But... how would I combine validation for several fields like this? It was possible in a root_validator. Example use case is to have an UploadFile and some_param in MyModel(BaseModel) and then try to check if the uploaded file name contains some_param.

mrc
  • 126
  • 1
  • 11