Add a hook to intercept ValidationError

This commit is contained in:
Vincent Maillol 2021-07-26 08:12:38 +02:00
parent 81138cc1c6
commit 7ab2d84263
4 changed files with 100 additions and 8 deletions

View File

@ -310,6 +310,26 @@ Open Api Specification.
self.request.app["model"].remove_pet(id)
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
----

View File

@ -2,7 +2,7 @@ import abc
import typing
from inspect import signature
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_request import BaseRequest
@ -12,6 +12,9 @@ from pydantic import BaseModel
from .utils import is_pydantic_base_model
CONTEXT = Literal["body", "headers", "path", "query string"]
class AbstractInjector(metaclass=abc.ABCMeta):
"""
An injector parse HTTP request and inject params to the view.
@ -19,7 +22,7 @@ class AbstractInjector(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def context(self) -> str:
def context(self) -> CONTEXT:
"""
The name of part of parsed request
i.e "HTTP header", "URL path", ...

View File

@ -1,6 +1,6 @@
from functools import update_wrapper
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
from aiohttp.abc import AbstractView
@ -17,6 +17,7 @@ from .injectors import (
MatchInfoGetter,
QueryGetter,
_parse_func_signature,
CONTEXT,
)
@ -88,6 +89,22 @@ class PydanticView(AbstractView):
injectors.append(HeadersGetter(header_args, default_value(header_args)))
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(
handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]]
@ -109,11 +126,7 @@ def inject_params(
else:
injector.inject(self.request, args, kwargs)
except ValidationError as error:
errors = error.errors()
for error in errors:
error["in"] = injector.context
return json_response(data=errors, status=400)
return await self.on_validation_error(error, injector.context)
return await handler(self, *args, **kwargs)

View 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",
}
]