Skip to content
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 custom error responses to be generated #56

Merged
merged 2 commits into from
Sep 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ For more complete examples see [example application](https://github.com/bauerji/
The behaviour can be configured using flask's application config
`FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE` - response status code after validation error (defaults to `400`)

Additionally, you can set `FLASK_PYDANTIC_VALIDATION_ERROR_RAISE` to `True` to cause
`flask_pydantic.ValidationError` to be raised with either `body_params`,
`form_params`, `path_params`, or `query_params` set as a list of error
dictionaries. You can use `flask.Flask.register_error_handler` to catch that
exception and fully customize the output response for a validation error.

## Contributing

Feature requests and pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Expand Down
1 change: 1 addition & 0 deletions flask_pydantic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .core import validate # noqa: F401
from .exceptions import ValidationError # noqa: F401
from .version import __version__ # noqa: F401
17 changes: 13 additions & 4 deletions flask_pydantic/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
JsonBodyParsingError,
ManyModelValidationError,
)
from .exceptions import ValidationError as FailedValidation

try:
from flask_restful import original_flask_make_response as make_response
Expand Down Expand Up @@ -235,10 +236,18 @@ def wrapper(*args, **kwargs):
kwargs["form"] = f

if err:
status_code = current_app.config.get(
"FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE", 400
)
return make_response(jsonify({"validation_error": err}), status_code)
if current_app.config.get(
"FLASK_PYDANTIC_VALIDATION_ERROR_RAISE", False
):
raise FailedValidation(**err)
else:
status_code = current_app.config.get(
"FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE", 400
)
return make_response(
jsonify({"validation_error": err}),
status_code
)
res = func(*args, **kwargs)

if response_many:
Expand Down
20 changes: 19 additions & 1 deletion flask_pydantic/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional


class BaseFlaskPydanticException(Exception):
Expand Down Expand Up @@ -30,3 +30,21 @@ def __init__(self, errors: List[dict], *args):

def errors(self):
return self._errors


class ValidationError(BaseFlaskPydanticException):
"""This exception is raised if there is a failure during validation if the
user has configured an exception to be raised instead of a response"""

def __init__(
self,
body_params: Optional[List[dict]] = None,
form_params: Optional[List[dict]] = None,
path_params: Optional[List[dict]] = None,
query_params: Optional[List[dict]] = None,
):
super().__init__()
self.body_params = body_params
self.form_params = form_params
self.path_params = path_params
self.query_params = query_params
46 changes: 44 additions & 2 deletions tests/func/test_app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import List, Optional

import pytest
from flask import request
from flask_pydantic import validate
from flask import jsonify, request
from flask_pydantic import validate, ValidationError
from pydantic import BaseModel


Expand Down Expand Up @@ -37,6 +37,32 @@ def silent(body: Body):
return body


@pytest.fixture
def app_raise_on_validation_error(app):
app.config["FLASK_PYDANTIC_VALIDATION_ERROR_RAISE"] = True

def validation_error(error: ValidationError):
return (
jsonify(
{
"title": "validation error",
"body": error.body_params,
}
),
422,
)

app.register_error_handler(ValidationError, validation_error)

class Body(BaseModel):
param: str

@app.route("/silent", methods=["POST"])
@validate(get_json_params={"silent": True})
def silent(body: Body):
return body


@pytest.fixture
def app_with_int_path_param_route(app):
class IdObj(BaseModel):
Expand Down Expand Up @@ -388,3 +414,19 @@ def test_silent(self, client):
}
}
assert response.status_code == 400


@pytest.mark.usefixtures("app_raise_on_validation_error")
class TestCustomResponse:
def test_silent(self, client):
response = client.post("/silent", headers={"Content-Type": "application/json"})

assert response.json["title"] == "validation error"
assert response.json["body"] == [
{
"loc": ["param"],
"msg": "field required",
"type": "value_error.missing",
}
]
assert response.status_code == 422
74 changes: 73 additions & 1 deletion tests/unit/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest
from flask import jsonify
from flask_pydantic import validate
from flask_pydantic import validate, ValidationError
from flask_pydantic.core import convert_query_params, is_iterable_of_models
from flask_pydantic.exceptions import (
InvalidIterableOfModelsException,
Expand Down Expand Up @@ -429,6 +429,78 @@ def f() -> Any:
exclude_none=True, exclude_defaults=True
) == parameters.request_query.to_dict(flat=True)

def test_fail_validation_custom_status_code(self, app, request_ctx, mocker):
app.config["FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE"] = 422
mock_request = mocker.patch.object(request_ctx, "request")
content_type = "application/json"
mock_request.headers = {"Content-Type": content_type}
mock_request.get_json = lambda: None
body_model = RequestBodyModelRoot
response = validate(body_model)(lambda x: x)()
assert response.status_code == 422
assert response.json == {
"validation_error": {
"body_params": [
{
"loc": ["__root__"],
"msg": "none is not an allowed value",
"type": "type_error.none.not_allowed",
}
]
}
}

def test_body_fail_validation_raise_exception(self, app, request_ctx, mocker):
app.config["FLASK_PYDANTIC_VALIDATION_ERROR_RAISE"] = True
mock_request = mocker.patch.object(request_ctx, "request")
content_type = "application/json"
mock_request.headers = {"Content-Type": content_type}
mock_request.get_json = lambda: None
body_model = RequestBodyModelRoot
with pytest.raises(ValidationError) as excinfo:
response = validate(body_model)(lambda x: x)()
assert excinfo.value.body_params == [
{
"loc": ("__root__",),
"msg": "none is not an allowed value",
"type": "type_error.none.not_allowed",
}
]

def test_query_fail_validation_raise_exception(self, app, request_ctx, mocker):
app.config["FLASK_PYDANTIC_VALIDATION_ERROR_RAISE"] = True
mock_request = mocker.patch.object(request_ctx, "request")
content_type = "application/json"
mock_request.headers = {"Content-Type": content_type}
mock_request.get_json = lambda: None
query_model = QueryModel
with pytest.raises(ValidationError) as excinfo:
response = validate(query=query_model)(lambda x: x)()
assert excinfo.value.query_params == [
{
"loc": ("q1",),
"msg": "field required",
"type": "value_error.missing",
}
]

def test_form_fail_validation_raise_exception(self, app, request_ctx, mocker):
app.config["FLASK_PYDANTIC_VALIDATION_ERROR_RAISE"] = True
mock_request = mocker.patch.object(request_ctx, "request")
content_type = "application/json"
mock_request.headers = {"Content-Type": content_type}
mock_request.get_json = lambda: None
form_model = FormModel
with pytest.raises(ValidationError) as excinfo:
response = validate(form=form_model)(lambda x: x)()
assert excinfo.value.form_params == [
{
"loc": ("f1",),
"msg": "field required",
"type": "value_error.missing",
}
]


class TestIsIterableOfModels:
def test_simple_true_case(self):
Expand Down