From fea09a47ad4e8cd6286b9faf433c58f48c7e496f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Bregu=C5=82a?= Date: Wed, 19 Mar 2025 18:02:18 +0000 Subject: [PATCH 1/6] Add django decorator --- docs/integrations/django.md | 41 +++++++++ openapi_core/contrib/django/decorators.py | 89 +++++++++++++++++++ openapi_core/contrib/django/middlewares.py | 18 +--- openapi_core/contrib/django/providers.py | 27 ++++++ .../v3.0/djangoproject/status/__init__.py | 0 .../status/migrations/__init__.py | 0 .../data/v3.0/djangoproject/status/views.py | 16 ++++ .../django/data/v3.0/djangoproject/urls.py | 6 ++ .../contrib/django/test_django_project.py | 38 ++++++++ 9 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 openapi_core/contrib/django/decorators.py create mode 100644 openapi_core/contrib/django/providers.py create mode 100644 tests/integration/contrib/django/data/v3.0/djangoproject/status/__init__.py create mode 100644 tests/integration/contrib/django/data/v3.0/djangoproject/status/migrations/__init__.py create mode 100644 tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py diff --git a/docs/integrations/django.md b/docs/integrations/django.md index 8369ea95..0b795bb8 100644 --- a/docs/integrations/django.md +++ b/docs/integrations/django.md @@ -57,6 +57,47 @@ OPENAPI = OpenAPI.from_dict(spec_dict) OPENAPI_RESPONSE_CLS = None ``` +## Decorator + +Django can be integrated using [view decorators](https://docs.djangoproject.com/en/5.1/topics/http/decorators/) to apply OpenAPI validation to your application's specific views. + +Use `DjangoOpenAPIViewDecorator` with the OpenAPI object to create the decorator. + +``` python hl_lines="1 3 6" +from openapi_core.contrib.django.decorators import DjangoOpenAPIViewDecorator + +openapi_validated = FlaskOpenAPIViewDecorator(openapi) + + +@openapi_validated +def home(): + return "Welcome home" +``` + +You can skip the response validation process by setting `response_cls` to `None`. + +``` python hl_lines="5" +from openapi_core.contrib.django.decorators import DjangoOpenAPIViewDecorator + +openapi_validated = DjangoOpenAPIViewDecorator( + openapi, + response_cls=None, +) +``` + +If you want to decorate a class-based view, you can use the `method_decorator` decorator: + +``` python hl_lines="2" +from django.utils.decorators import method_decorator + +@method_decorator(openapi_validated, name='dispatch') +class MyView(View): + decorators = [openapi_validated] + + def get(self, request, *args, **kwargs): + return "Welcome home" +``` + ## Low level The integration defines classes useful for low-level integration. diff --git a/openapi_core/contrib/django/decorators.py b/openapi_core/contrib/django/decorators.py new file mode 100644 index 00000000..6bf83ac6 --- /dev/null +++ b/openapi_core/contrib/django/decorators.py @@ -0,0 +1,89 @@ +"""OpenAPI core contrib django decorators module""" +from typing import Type + +from django.conf import settings +from django.http.request import HttpRequest +from django.http.response import HttpResponse + +from jsonschema_path import SchemaPath + +from openapi_core import OpenAPI +from openapi_core.contrib.django.integrations import DjangoIntegration +from openapi_core.contrib.django.requests import DjangoOpenAPIRequest +from openapi_core.contrib.django.responses import DjangoOpenAPIResponse +from openapi_core.contrib.django.handlers import DjangoOpenAPIErrorsHandler +from openapi_core.contrib.django.handlers import ( + DjangoOpenAPIValidRequestHandler, +) + +class DjangoOpenAPIDecorator(DjangoIntegration): + valid_request_handler_cls = DjangoOpenAPIValidRequestHandler + errors_handler_cls: Type[DjangoOpenAPIErrorsHandler] = DjangoOpenAPIErrorsHandler + + def __init__( + self, + openapi: OpenAPI == None, + request_cls: Type[DjangoOpenAPIRequest] = DjangoOpenAPIRequest, + response_cls: Type[DjangoOpenAPIResponse] = DjangoOpenAPIResponse, + errors_handler_cls: Type[ + DjangoOpenAPIErrorsHandler + ] = DjangoOpenAPIErrorsHandler + ): + if openapi is None: + openapi = get_default_openapi_instance() + + super().__init__(openapi) + + # If OPENAPI_RESPONSE_CLS is defined in settings.py (for custom response classes), + # set the response_cls accordingly. + if hasattr(settings, "OPENAPI_RESPONSE_CLS"): + response_cls = settings.OPENAPI_RESPONSE_CLS + + self.request_cls = request_cls + self.response_cls = response_cls + + def __call__(self, view_func): + """ + Thanks to this method, the class acts as a decorator. + Example usage: + + @DjangoOpenAPIDecorator() + def my_view(request): ... + + """ + + def _wrapped_view(request: HttpRequest, *args, **kwargs) -> HttpResponse: + # get_response is the function that we treats + # as the "next step" in the chain (i.e., our original view). + def get_response(r: HttpRequest) -> HttpResponse: + return view_func(r, *args, **kwargs) + + # Create a handler that will validate the request. + valid_request_handler = self.valid_request_handler_cls(request, get_response) + + # Validate the request (before running the view). + errors_handler = self.errors_handler_cls() + response = self.handle_request(request, valid_request_handler, errors_handler) + + # Validate the response (after the view) if should_validate_response() returns True. + return self.handle_response(request, response, errors_handler) + + return _wrapped_view + + @classmethod + def from_spec( + cls, + spec: SchemaPath, + request_cls: Type[DjangoOpenAPIRequest] = DjangoOpenAPIRequest, + response_cls: Type[DjangoOpenAPIResponse] = DjangoOpenAPIResponse, + errors_handler_cls: Type[ + DjangoOpenAPIErrorsHandler + ] = DjangoOpenAPIErrorsHandler, + ) -> "DjangoOpenAPIViewDecorator": + openapi = OpenAPI(spec) + return cls( + openapi, + request_cls=request_cls, + response_cls=response_cls, + errors_handler_cls=errors_handler_cls, + ) \ No newline at end of file diff --git a/openapi_core/contrib/django/middlewares.py b/openapi_core/contrib/django/middlewares.py index 35b865bd..34ffe273 100644 --- a/openapi_core/contrib/django/middlewares.py +++ b/openapi_core/contrib/django/middlewares.py @@ -1,19 +1,17 @@ """OpenAPI core contrib django middlewares module""" -import warnings from typing import Callable from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.http.request import HttpRequest from django.http.response import HttpResponse -from openapi_core import OpenAPI from openapi_core.contrib.django.handlers import DjangoOpenAPIErrorsHandler from openapi_core.contrib.django.handlers import ( DjangoOpenAPIValidRequestHandler, ) from openapi_core.contrib.django.integrations import DjangoIntegration +from openapi_core.contrib.django.providers import get_default_openapi_instance class DjangoOpenAPIMiddleware(DjangoIntegration): @@ -26,19 +24,7 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): if hasattr(settings, "OPENAPI_RESPONSE_CLS"): self.response_cls = settings.OPENAPI_RESPONSE_CLS - if not hasattr(settings, "OPENAPI"): - if not hasattr(settings, "OPENAPI_SPEC"): - raise ImproperlyConfigured( - "OPENAPI_SPEC not defined in settings" - ) - else: - warnings.warn( - "OPENAPI_SPEC is deprecated. Use OPENAPI instead.", - DeprecationWarning, - ) - openapi = OpenAPI(settings.OPENAPI_SPEC) - else: - openapi = settings.OPENAPI + openapi = get_default_openapi_instance() super().__init__(openapi) diff --git a/openapi_core/contrib/django/providers.py b/openapi_core/contrib/django/providers.py new file mode 100644 index 00000000..a1801d07 --- /dev/null +++ b/openapi_core/contrib/django/providers.py @@ -0,0 +1,27 @@ +"""OpenAPI core contrib django providers module""" + +import warnings + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +from openapi_core import OpenAPI + +def get_default_openapi_instance() -> OpenAPI: + """ + Retrieves or initializes the OpenAPI instance based on Django settings + (either OPENAPI or OPENAPI_SPEC). + This function ensures the spec is only loaded once. + """ + if hasattr(settings, "OPENAPI"): + # Recommended (newer) approach + return settings.OPENAPI + elif hasattr(settings, "OPENAPI_SPEC"): + # Backward compatibility + warnings.warn( + "OPENAPI_SPEC is deprecated. Use OPENAPI in your settings instead.", + DeprecationWarning, + ) + return OpenAPI(settings.OPENAPI_SPEC) + else: + raise ImproperlyConfigured("Neither OPENAPI nor OPENAPI_SPEC is defined in Django settings.") \ No newline at end of file diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/__init__.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/migrations/__init__.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py new file mode 100644 index 00000000..def60e75 --- /dev/null +++ b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from django.http import HttpResponse +from openapi_core.contrib.django.decorators import DjangoOpenAPIDecorator +from jsonschema_path import SchemaPath + + +check_minimal_spec = DjangoOpenAPIDecorator.from_spec( + SchemaPath.from_file_path( + Path("tests/integration/data/v3.0/minimal_with_servers.yaml") + ) +) + +@check_minimal_spec +def get_status(request): + return HttpResponse("OK") \ No newline at end of file diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py b/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py index ff987972..be4e9781 100644 --- a/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py +++ b/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py @@ -20,6 +20,7 @@ from djangoproject.pets.views import PetDetailView from djangoproject.pets.views import PetListView from djangoproject.pets.views import PetPhotoView +from djangoproject.status.views import get_status from djangoproject.tags.views import TagListView urlpatterns = [ @@ -48,4 +49,9 @@ TagListView.as_view(), name="tag_list_view", ), + path( + "status", + get_status, + name="get_status_view", + ), ] diff --git a/tests/integration/contrib/django/test_django_project.py b/tests/integration/contrib/django/test_django_project.py index 6614eeaf..4604db52 100644 --- a/tests/integration/contrib/django/test_django_project.py +++ b/tests/integration/contrib/django/test_django_project.py @@ -422,3 +422,41 @@ def test_post_valid(self, client, data_gif): assert response.status_code == 201 assert not response.content + + +class TestStatusView(BaseTestDjangoProject): + + def test_get_valid(self, client, data_gif): + headers = { + "HTTP_AUTHORIZATION": "Basic testuser", + "HTTP_HOST": "petstore.swagger.io", + } + from django.conf import settings + + MIDDLEWARE = [ + v for v in settings.MIDDLEWARE if "openapi_core" not in v + ] + with override_settings(MIDDLEWARE=MIDDLEWARE): + response = client.get("/status", **headers) + + assert response.status_code == 200 + assert response.content.decode() == "OK" + + def test_post_valid(self, client): + data = {"key": "value"} + content_type = "application/json" + headers = { + "HTTP_AUTHORIZATION": "Basic testuser", + "HTTP_HOST": "petstore.swagger.io", + } + from django.conf import settings + + MIDDLEWARE = [ + v for v in settings.MIDDLEWARE if "openapi_core" not in v + ] + with override_settings(MIDDLEWARE=MIDDLEWARE): + response = client.post( + "/status", data=data, content_type=content_type, **headers + ) + + assert response.status_code == 405 # Method Not Allowed From 6b45fb06721611a202a7543817a048dc84191ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Bregu=C5=82a?= Date: Wed, 19 Mar 2025 18:07:12 +0000 Subject: [PATCH 2/6] fixup! Add django decorator --- openapi_core/contrib/django/decorators.py | 31 ++++++++++++------- openapi_core/contrib/django/providers.py | 5 ++- .../data/v3.0/djangoproject/status/views.py | 7 +++-- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/openapi_core/contrib/django/decorators.py b/openapi_core/contrib/django/decorators.py index 6bf83ac6..5c7bd0fd 100644 --- a/openapi_core/contrib/django/decorators.py +++ b/openapi_core/contrib/django/decorators.py @@ -1,33 +1,36 @@ """OpenAPI core contrib django decorators module""" + from typing import Type from django.conf import settings from django.http.request import HttpRequest from django.http.response import HttpResponse - from jsonschema_path import SchemaPath from openapi_core import OpenAPI -from openapi_core.contrib.django.integrations import DjangoIntegration -from openapi_core.contrib.django.requests import DjangoOpenAPIRequest -from openapi_core.contrib.django.responses import DjangoOpenAPIResponse from openapi_core.contrib.django.handlers import DjangoOpenAPIErrorsHandler from openapi_core.contrib.django.handlers import ( DjangoOpenAPIValidRequestHandler, ) +from openapi_core.contrib.django.integrations import DjangoIntegration +from openapi_core.contrib.django.requests import DjangoOpenAPIRequest +from openapi_core.contrib.django.responses import DjangoOpenAPIResponse + class DjangoOpenAPIDecorator(DjangoIntegration): valid_request_handler_cls = DjangoOpenAPIValidRequestHandler - errors_handler_cls: Type[DjangoOpenAPIErrorsHandler] = DjangoOpenAPIErrorsHandler + errors_handler_cls: Type[DjangoOpenAPIErrorsHandler] = ( + DjangoOpenAPIErrorsHandler + ) def __init__( - self, + self, openapi: OpenAPI == None, request_cls: Type[DjangoOpenAPIRequest] = DjangoOpenAPIRequest, response_cls: Type[DjangoOpenAPIResponse] = DjangoOpenAPIResponse, errors_handler_cls: Type[ DjangoOpenAPIErrorsHandler - ] = DjangoOpenAPIErrorsHandler + ] = DjangoOpenAPIErrorsHandler, ): if openapi is None: openapi = get_default_openapi_instance() @@ -52,18 +55,24 @@ def my_view(request): ... """ - def _wrapped_view(request: HttpRequest, *args, **kwargs) -> HttpResponse: + def _wrapped_view( + request: HttpRequest, *args, **kwargs + ) -> HttpResponse: # get_response is the function that we treats # as the "next step" in the chain (i.e., our original view). def get_response(r: HttpRequest) -> HttpResponse: return view_func(r, *args, **kwargs) # Create a handler that will validate the request. - valid_request_handler = self.valid_request_handler_cls(request, get_response) + valid_request_handler = self.valid_request_handler_cls( + request, get_response + ) # Validate the request (before running the view). errors_handler = self.errors_handler_cls() - response = self.handle_request(request, valid_request_handler, errors_handler) + response = self.handle_request( + request, valid_request_handler, errors_handler + ) # Validate the response (after the view) if should_validate_response() returns True. return self.handle_response(request, response, errors_handler) @@ -86,4 +95,4 @@ def from_spec( request_cls=request_cls, response_cls=response_cls, errors_handler_cls=errors_handler_cls, - ) \ No newline at end of file + ) diff --git a/openapi_core/contrib/django/providers.py b/openapi_core/contrib/django/providers.py index a1801d07..f698bfcf 100644 --- a/openapi_core/contrib/django/providers.py +++ b/openapi_core/contrib/django/providers.py @@ -7,6 +7,7 @@ from openapi_core import OpenAPI + def get_default_openapi_instance() -> OpenAPI: """ Retrieves or initializes the OpenAPI instance based on Django settings @@ -24,4 +25,6 @@ def get_default_openapi_instance() -> OpenAPI: ) return OpenAPI(settings.OPENAPI_SPEC) else: - raise ImproperlyConfigured("Neither OPENAPI nor OPENAPI_SPEC is defined in Django settings.") \ No newline at end of file + raise ImproperlyConfigured( + "Neither OPENAPI nor OPENAPI_SPEC is defined in Django settings." + ) diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py index def60e75..96e416b0 100644 --- a/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py +++ b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py @@ -1,9 +1,9 @@ from pathlib import Path from django.http import HttpResponse -from openapi_core.contrib.django.decorators import DjangoOpenAPIDecorator from jsonschema_path import SchemaPath +from openapi_core.contrib.django.decorators import DjangoOpenAPIDecorator check_minimal_spec = DjangoOpenAPIDecorator.from_spec( SchemaPath.from_file_path( @@ -11,6 +11,7 @@ ) ) + @check_minimal_spec -def get_status(request): - return HttpResponse("OK") \ No newline at end of file +def get_status(request): + return HttpResponse("OK") From c3e4c1bb613fd493789586a4f415ce052fe9cb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Bregu=C5=82a?= Date: Wed, 19 Mar 2025 18:09:06 +0000 Subject: [PATCH 3/6] fixup! fixup! Add django decorator --- openapi_core/contrib/django/decorators.py | 5 +++-- .../contrib/django/data/v3.0/djangoproject/status/views.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openapi_core/contrib/django/decorators.py b/openapi_core/contrib/django/decorators.py index 5c7bd0fd..f64d1243 100644 --- a/openapi_core/contrib/django/decorators.py +++ b/openapi_core/contrib/django/decorators.py @@ -13,11 +13,12 @@ DjangoOpenAPIValidRequestHandler, ) from openapi_core.contrib.django.integrations import DjangoIntegration +from openapi_core.contrib.django.providers import get_default_openapi_instance from openapi_core.contrib.django.requests import DjangoOpenAPIRequest from openapi_core.contrib.django.responses import DjangoOpenAPIResponse -class DjangoOpenAPIDecorator(DjangoIntegration): +class DjangoOpenAPIViewDecorator(DjangoIntegration): valid_request_handler_cls = DjangoOpenAPIValidRequestHandler errors_handler_cls: Type[DjangoOpenAPIErrorsHandler] = ( DjangoOpenAPIErrorsHandler @@ -50,7 +51,7 @@ def __call__(self, view_func): Thanks to this method, the class acts as a decorator. Example usage: - @DjangoOpenAPIDecorator() + @DjangoOpenAPIViewDecorator() def my_view(request): ... """ diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py index 96e416b0..10d87749 100644 --- a/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py +++ b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py @@ -3,9 +3,9 @@ from django.http import HttpResponse from jsonschema_path import SchemaPath -from openapi_core.contrib.django.decorators import DjangoOpenAPIDecorator +from openapi_core.contrib.django.decorators import DjangoOpenAPIViewDecorator -check_minimal_spec = DjangoOpenAPIDecorator.from_spec( +check_minimal_spec = DjangoOpenAPIViewDecorator.from_spec( SchemaPath.from_file_path( Path("tests/integration/data/v3.0/minimal_with_servers.yaml") ) From 098c0d103caae6af12f7cd417cc81e4dc333f26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Bregu=C5=82a?= Date: Wed, 19 Mar 2025 18:17:21 +0000 Subject: [PATCH 4/6] fixup! fixup! fixup! Add django decorator --- openapi_core/contrib/django/decorators.py | 9 ++++++--- openapi_core/contrib/django/providers.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openapi_core/contrib/django/decorators.py b/openapi_core/contrib/django/decorators.py index f64d1243..f6be3cbf 100644 --- a/openapi_core/contrib/django/decorators.py +++ b/openapi_core/contrib/django/decorators.py @@ -1,5 +1,8 @@ """OpenAPI core contrib django decorators module""" +from typing import Any +from typing import Callable +from typing import Optional from typing import Type from django.conf import settings @@ -26,7 +29,7 @@ class DjangoOpenAPIViewDecorator(DjangoIntegration): def __init__( self, - openapi: OpenAPI == None, + openapi: Optional[OpenAPI] = None, request_cls: Type[DjangoOpenAPIRequest] = DjangoOpenAPIRequest, response_cls: Type[DjangoOpenAPIResponse] = DjangoOpenAPIResponse, errors_handler_cls: Type[ @@ -46,7 +49,7 @@ def __init__( self.request_cls = request_cls self.response_cls = response_cls - def __call__(self, view_func): + def __call__(self, view_func: Callable[..., Any]) -> Callable[..., Any]: """ Thanks to this method, the class acts as a decorator. Example usage: @@ -57,7 +60,7 @@ def my_view(request): ... """ def _wrapped_view( - request: HttpRequest, *args, **kwargs + request: HttpRequest, *args: Any, **kwargs: Any ) -> HttpResponse: # get_response is the function that we treats # as the "next step" in the chain (i.e., our original view). diff --git a/openapi_core/contrib/django/providers.py b/openapi_core/contrib/django/providers.py index f698bfcf..cb4f2a73 100644 --- a/openapi_core/contrib/django/providers.py +++ b/openapi_core/contrib/django/providers.py @@ -1,6 +1,7 @@ """OpenAPI core contrib django providers module""" import warnings +from typing import cast from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -16,7 +17,7 @@ def get_default_openapi_instance() -> OpenAPI: """ if hasattr(settings, "OPENAPI"): # Recommended (newer) approach - return settings.OPENAPI + return cast(OpenAPI, settings.OPENAPI) elif hasattr(settings, "OPENAPI_SPEC"): # Backward compatibility warnings.warn( From 4842992d57bbd5e6553ab428cfa3e5b72e7a2c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Bregu=C5=82a?= Date: Wed, 19 Mar 2025 19:36:27 +0100 Subject: [PATCH 5/6] Update docs/integrations/django.md Co-authored-by: Artur Maciag --- docs/integrations/django.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/django.md b/docs/integrations/django.md index 0b795bb8..c6692f8a 100644 --- a/docs/integrations/django.md +++ b/docs/integrations/django.md @@ -87,7 +87,7 @@ openapi_validated = DjangoOpenAPIViewDecorator( If you want to decorate a class-based view, you can use the `method_decorator` decorator: -``` python hl_lines="2" +``` python hl_lines="3" from django.utils.decorators import method_decorator @method_decorator(openapi_validated, name='dispatch') From ca6044ef295902234d62b631942fe659374db857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Bregu=C5=82a?= Date: Wed, 19 Mar 2025 19:44:50 +0100 Subject: [PATCH 6/6] Update django.md --- docs/integrations/django.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/integrations/django.md b/docs/integrations/django.md index c6692f8a..00c6fef4 100644 --- a/docs/integrations/django.md +++ b/docs/integrations/django.md @@ -92,7 +92,6 @@ from django.utils.decorators import method_decorator @method_decorator(openapi_validated, name='dispatch') class MyView(View): - decorators = [openapi_validated] def get(self, request, *args, **kwargs): return "Welcome home"