Add a hook to intercept ValidationError
This commit is contained in:
parent
81138cc1c6
commit
7ab2d84263
20
README.rst
20
README.rst
@ -310,6 +310,26 @@ Open Api Specification.
|
|||||||
self.request.app["model"].remove_pet(id)
|
self.request.app["model"].remove_pet(id)
|
||||||
return web.Response(status=204)
|
return web.Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
|
Custom Validation error
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
You can redefine the on_validation_error hook in your PydanticView
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
class PetView(PydanticView):
|
||||||
|
|
||||||
|
async def on_validation_error(self,
|
||||||
|
exception: ValidationError,
|
||||||
|
context: str):
|
||||||
|
errors = exception.errors()
|
||||||
|
for error in errors:
|
||||||
|
error["in"] = context # context is "body", "headers", "path" or "query string"
|
||||||
|
error["custom"] = "your custom field ..."
|
||||||
|
return json_response(data=errors, status=400)
|
||||||
|
|
||||||
|
|
||||||
Demo
|
Demo
|
||||||
----
|
----
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import abc
|
|||||||
import typing
|
import typing
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
from typing import Callable, Tuple
|
from typing import Callable, Tuple, Literal
|
||||||
|
|
||||||
from aiohttp.web_exceptions import HTTPBadRequest
|
from aiohttp.web_exceptions import HTTPBadRequest
|
||||||
from aiohttp.web_request import BaseRequest
|
from aiohttp.web_request import BaseRequest
|
||||||
@ -12,6 +12,9 @@ from pydantic import BaseModel
|
|||||||
from .utils import is_pydantic_base_model
|
from .utils import is_pydantic_base_model
|
||||||
|
|
||||||
|
|
||||||
|
CONTEXT = Literal["body", "headers", "path", "query string"]
|
||||||
|
|
||||||
|
|
||||||
class AbstractInjector(metaclass=abc.ABCMeta):
|
class AbstractInjector(metaclass=abc.ABCMeta):
|
||||||
"""
|
"""
|
||||||
An injector parse HTTP request and inject params to the view.
|
An injector parse HTTP request and inject params to the view.
|
||||||
@ -19,7 +22,7 @@ class AbstractInjector(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def context(self) -> str:
|
def context(self) -> CONTEXT:
|
||||||
"""
|
"""
|
||||||
The name of part of parsed request
|
The name of part of parsed request
|
||||||
i.e "HTTP header", "URL path", ...
|
i.e "HTTP header", "URL path", ...
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from inspect import iscoroutinefunction
|
from inspect import iscoroutinefunction
|
||||||
from typing import Any, Callable, Generator, Iterable, Set, ClassVar
|
from typing import Any, Callable, Generator, Iterable, Set, ClassVar, Literal
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from aiohttp.abc import AbstractView
|
from aiohttp.abc import AbstractView
|
||||||
@ -17,6 +17,7 @@ from .injectors import (
|
|||||||
MatchInfoGetter,
|
MatchInfoGetter,
|
||||||
QueryGetter,
|
QueryGetter,
|
||||||
_parse_func_signature,
|
_parse_func_signature,
|
||||||
|
CONTEXT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -88,6 +89,22 @@ class PydanticView(AbstractView):
|
|||||||
injectors.append(HeadersGetter(header_args, default_value(header_args)))
|
injectors.append(HeadersGetter(header_args, default_value(header_args)))
|
||||||
return injectors
|
return injectors
|
||||||
|
|
||||||
|
async def on_validation_error(
|
||||||
|
self, exception: ValidationError, context: CONTEXT
|
||||||
|
) -> StreamResponse:
|
||||||
|
"""
|
||||||
|
This method is a hook to intercept ValidationError.
|
||||||
|
|
||||||
|
This hook can be redefined to return a custom HTTP response error.
|
||||||
|
The exception is a pydantic.ValidationError and the context is "body",
|
||||||
|
"headers", "path" or "query string"
|
||||||
|
"""
|
||||||
|
errors = exception.errors()
|
||||||
|
for error in errors:
|
||||||
|
error["in"] = context
|
||||||
|
|
||||||
|
return json_response(data=errors, status=400)
|
||||||
|
|
||||||
|
|
||||||
def inject_params(
|
def inject_params(
|
||||||
handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]]
|
handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]]
|
||||||
@ -109,11 +126,7 @@ def inject_params(
|
|||||||
else:
|
else:
|
||||||
injector.inject(self.request, args, kwargs)
|
injector.inject(self.request, args, kwargs)
|
||||||
except ValidationError as error:
|
except ValidationError as error:
|
||||||
errors = error.errors()
|
return await self.on_validation_error(error, injector.context)
|
||||||
for error in errors:
|
|
||||||
error["in"] = injector.context
|
|
||||||
|
|
||||||
return json_response(data=errors, status=400)
|
|
||||||
|
|
||||||
return await handler(self, *args, **kwargs)
|
return await handler(self, *args, **kwargs)
|
||||||
|
|
||||||
|
56
tests/test_hook_to_custom_response.py
Normal file
56
tests/test_hook_to_custom_response.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from typing import Iterator, List, Optional
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from aiohttp.web_response import json_response
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from aiohttp_pydantic import PydanticView
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleModel(BaseModel):
|
||||||
|
name: str
|
||||||
|
nb_page: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleModels(BaseModel):
|
||||||
|
__root__: List[ArticleModel]
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[ArticleModel]:
|
||||||
|
return iter(self.__root__)
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleView(PydanticView):
|
||||||
|
|
||||||
|
async def post(self, article: ArticleModel):
|
||||||
|
return web.json_response(article.dict())
|
||||||
|
|
||||||
|
async def put(self, articles: ArticleModels):
|
||||||
|
return web.json_response([article.dict() for article in articles])
|
||||||
|
|
||||||
|
async def on_validation_error(self, exception, context):
|
||||||
|
errors = exception.errors()
|
||||||
|
for error in errors:
|
||||||
|
error["in"] = context
|
||||||
|
error["custom"] = "custom"
|
||||||
|
return json_response(data=errors, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_an_article_with_wrong_type_field_should_return_an_error_message(
|
||||||
|
aiohttp_client, loop
|
||||||
|
):
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_view("/article", ArticleView)
|
||||||
|
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
resp = await client.post("/article", json={"name": "foo", "nb_page": "foo"})
|
||||||
|
assert resp.status == 400
|
||||||
|
assert resp.content_type == "application/json"
|
||||||
|
assert await resp.json() == [
|
||||||
|
{
|
||||||
|
"in": "body",
|
||||||
|
"loc": ["nb_page"],
|
||||||
|
"msg": "value is not a valid integer",
|
||||||
|
"custom": "custom",
|
||||||
|
"type": "type_error.integer",
|
||||||
|
}
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user