Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to raise RequestValidationError #471

Closed
sscherfke opened this issue Aug 27, 2019 · 9 comments
Closed

Allow users to raise RequestValidationError #471

sscherfke opened this issue Aug 27, 2019 · 9 comments

Comments

@sscherfke
Copy link

Sometimes, you have to perform additional validation of parameters (e.g., Path parameters) inside your API function. Currently, it is not very convenient to produce an error structured like the ones that the internal validation produces (e.g. when an int-typed param cannot be parsed).

Describe the solution you'd like

from fastapi import [Request]ValidationError

@app.post("/upload/{channel}")
async dev conda_upload(channel: str, file ...):
    if not meets_some_condition(channel):
        raise RequestValidationError(
            ["path", "channel"], 
            f"Channel '{channel}' does not match some contion",
            "value_error.str.mycondition",  # maybe this can be optional
            # **extra,  # Additional information
        )

Describe alternatives you've considered
You can explicitly construct the JSON structure. But you need to remember the correct responsec code and the correct JSON structure:

from fastapi import HTTPException

@app.post("/upload/{channel}")
async dev conda_upload(channel: str, file ...):
    if not meets_some_condition(channel)
        raise HTTPException(
            422,
            [{
                'loc': ["path", 'channel'],
                'msg': f"Channel '{channel}' does not match some condition",
                'type': 'value_error.str.condition',
            }]
        )

Additional context
Add any other context or screenshots about the feature request here.

@sscherfke
Copy link
Author

sscherfke commented Sep 2, 2019

This would be the simplest possible solution:

class RequestValidationError(HTTPException):
    def __init__(self, loc, msg, typ):
        super().__init__(422, [{'loc': loc, 'msg': msg, 'type': typ}])

Passing loc as list of two elements bothers me for some reason, maybe it can be simplified by using a string with dot separatos (body.path instead of ['body', 'path']).

It is also not always obvious what to use for the type argument.

@tiangolo
Copy link
Owner

Those validations come directly from Pydantic.

So you could create a Pydantic model with a custom validator (with your custom logic), and validate your data with Pydantic, checking for validation errors, and extracting those errors.

For example:

from fastapi import FastAPI, HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, validator, ValidationError


class CondaChannel(BaseModel):
    channel: str

    @validator("channel")
    def channel_validator(cls, v: str):
        # Do some validation
        if not v.startswith("superawesomechannel"):
            raise ValueError("Only super awesome channels are supported.")
        return v


app = FastAPI()


@app.post("/upload/{channel}")
async def conda_upload(channel: str):
    try:
        CondaChannel(channel=channel)
    except ValidationError as err:
        raise HTTPException(status_code=422, detail=jsonable_encoder(err.errors()))

@github-actions
Copy link
Contributor

Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.

@rockallite
Copy link

Those validations come directly from Pydantic.

So you could create a Pydantic model with a custom validator (with your custom logic), and validate your data with Pydantic, checking for validation errors, and extracting those errors.

For example:

from fastapi import FastAPI, HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, validator, ValidationError


class CondaChannel(BaseModel):
    channel: str

    @validator("channel")
    def channel_validator(cls, v: str):
        # Do some validation
        if not v.startswith("superawesomechannel"):
            raise ValueError("Only super awesome channels are supported.")
        return v


app = FastAPI()


@app.post("/upload/{channel}")
async def conda_upload(channel: str):
    try:
        CondaChannel(channel=channel)
    except ValidationError as err:
        raise HTTPException(status_code=422, detail=jsonable_encoder(err.errors()))

The loc structure in the JSON response won't be correct then. It should be:

{
  "detail": [
    {
      "loc": [
        "path",
        "channel"
      ],
      "msg": "Channel 'foo' does not match some condition",
      "type": "value_error.str.condition"
    }
  ]
}

Instead, you will get:

{
  "detail": [
    {
      "loc": [
        "channel"
      ],
      "msg": "Only super awesome channels are supported.",
      "type": "value_error"
    }
  ]
}

@johtso
Copy link

johtso commented Apr 9, 2021

Just to chime in on this old issue.. it does seem like something is missing here for the use case where not everything fits neatly into pydantic model based validation.

A simple example is where the validation error doesn't come from your own validation logic, but instead from the response of a 3rd party API.

In that case, all you know is the field that had the issue and the validation error message and you want to feed that into the standard error response flow.

#3067

@arthurio
Copy link
Contributor

Something I did recently to add validation to an endpoint pending deprecation, that didn't have pydantic validation:

async def my_deprecated_endpoint(request: Request, data_as_dict: dict):
    try:
        # Reshape the data received using pydantic models
        data = DataAsModel(something=data_as_dict["a_nested_field"]["something"])
    except ValidationError as e:
        raise RequestValidationError(errors=e.raw_errors)  # This is the key piece

    # Rest of the logic for my endpoint...

Note that in my case it's safe to show to the client any error that happens when trying to format my data with the pydantic model. I would discourage having a wide try/except on the whole content of your endpoint, as the documentation says, for some errors, you don't want the client/users to be able to see the details of internal errors.

OscartGiles added a commit to alan-turing-institute/vehicle-grid-integration-webapp-private that referenced this issue Dec 2, 2021
response error (although not exactly the same)

- See tiangolo/fastapi#471
@bluebrown
Copy link

bluebrown commented Dec 18, 2021

I needed some custom validation for a query parameter that I split on comma and check if its on an Enum.

from pydantic.error_wrappers import ErrorWrapper

try:
    parts = [enum_type[s.upper()].value for s in value.split(",")]
except KeyError as e:
    en = enum_type.__name__.lower()
    err = f"Invalid {en} value: {str(e).lower()}"
    raise RequestValidationError([ErrorWrapper(ValueError(err), ("query", en))])

The error response with status 422 comes out like this

{
  "detail": [
    {
      "loc": [
        "query",
        "status"
      ],
      "msg": "Invalid status value: 'foo'",
      "type": "value_error"
    }
  ]
}

@antonagestam
Copy link

Combining some of the suggestions here I landed on this generic solution, that still works with OpenAPI by passing the parameter name as alias to Query.

T = TypeVar("T")


def query_list_parser(
    parameter_name: str,
    inner_type: type[T],
) -> Callable[[str], Awaitable[list[T]]]:
    async def parse_list(
        parameter: str = Query(Required, alias=parameter_name),
    ) -> list[T]:
        def parse() -> Iterator[T]:
            for index, value in enumerate(parameter.split(",")):
                try:
                    yield inner_type(value)
                except (TypeError, ValueError) as exception:
                    raise RequestValidationError(
                        [ErrorWrapper(exception, ("query", parameter_name, index))]
                    )
        return list(parse())
    return parse_list

@router.get("/")
async def view(ids: list[ID] = Depends(query_list_parser("ids", ID))) -> Response:
    ...

@blablatdinov
Copy link

Hello everyone!

I'm needed validating date by greater than param and not find this functional in fastapi.Query class.
So, I find next solution:

  1. Creating custom exception, which extend pydantic.errors.PydanticValueError
  2. Raising it in wraps by pydantic.error_wrappers.ErrorWrapper and fastapi.exceptions.RequestValidationError

Code example:

class DateTimeError(PydanticValueError):
    code = 'date.not_gt'
    msg_template = 'start date must be greater than 2020-07-09'


def _start_date(start_date: datetime.date):
    if start_date < datetime.date(2020, 7, 29):
        raise RequestValidationError(errors=[
            ErrorWrapper(
                DateTimeError(limit_value='2020-07-29'),
                loc=('query', 'start_date'),
            ),
        ])

    return start_date


@router.get('/graph-data/')
async def get_users_count_graph_data(
    start_date: datetime.date = Depends(_start_date),
    finish_date: datetime.date = None,
):
    ...

Invalid start_date response:

HTTP/1.1 422 Unprocessable Entity

{
    "detail": [
        {
            "ctx": {
                "limit_value": "2020-07-29"
            },
            "loc": [
                "query",
                "start_date"
            ],
            "msg": "start date must be greater than 2020-07-09",
            "type": "value_error.date.not_gt"
        }
    ]
}

I'm hope that this method can help you. Thanks!

@tiangolo tiangolo added question Question or problem answered reviewed and removed feature New feature or request labels Feb 22, 2023
@tiangolo tiangolo changed the title [FEATURE] Allow users to raise RequestValidationError Allow users to raise RequestValidationError Feb 24, 2023
@tiangolo tiangolo reopened this Feb 28, 2023
Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #8033 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

8 participants