From 799ab05f4d95bc5d6ae8b82de028c253736da8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=BC=C3=9Flein?= Date: Sat, 28 Jun 2025 22:07:32 +0100 Subject: [PATCH 1/4] pass-through for Schema-based results When the response-parameter is filled and the result is already a Schema we don't need to verify it again. --- ninja/operation.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/ninja/operation.py b/ninja/operation.py index e7b67ea6d..30efdb50e 100644 --- a/ninja/operation.py +++ b/ninja/operation.py @@ -275,26 +275,33 @@ def _result_to_response( # Empty response. return temporal_response - resp_object = ResponseObject(result) - # ^ we need object because getter_dict seems work only with model_validate - validated_object = response_model.model_validate( - resp_object, context={"request": request, "response_status": status} + model_dump_kwargs: Dict[str, Any] = dict( + by_alias=self.by_alias, + exclude_unset=self.exclude_unset, + exclude_defaults=self.exclude_defaults, + exclude_none=self.exclude_none, ) - - model_dump_kwargs: Dict[str, Any] = {} if pydantic_version >= [2, 7]: # pydantic added support for serialization context at 2.7 model_dump_kwargs.update( context={"request": request, "response_status": status} ) - result = validated_object.model_dump( - by_alias=self.by_alias, - exclude_unset=self.exclude_unset, - exclude_defaults=self.exclude_defaults, - exclude_none=self.exclude_none, - **model_dump_kwargs, - )["response"] + if isinstance(result, Schema): + # if the result is already a Schema, just return it + return self.api.create_response( + request, + result.model_dump(**model_dump_kwargs), + temporal_response=temporal_response, + ) + + resp_object = ResponseObject(result) + # ^ we need object because getter_dict seems work only with model_validate + validated_object = response_model.model_validate( + resp_object, context={"request": request, "response_status": status} + ) + + result = validated_object.model_dump(**model_dump_kwargs)["response"] return self.api.create_response( request, result, temporal_response=temporal_response ) From 7b1b941b75a6892bbc97b68d8ff9b784b992b0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=BC=C3=9Flein?= Date: Sat, 15 Nov 2025 12:55:09 +0000 Subject: [PATCH 2/4] allow for any BaseModel-inheriting Model, not just Schema-inheriting --- ninja/operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ninja/operation.py b/ninja/operation.py index 7fdf7bdd4..48faa0fb6 100644 --- a/ninja/operation.py +++ b/ninja/operation.py @@ -287,7 +287,7 @@ def _result_to_response( context={"request": request, "response_status": status} ) - if isinstance(result, Schema): + if isinstance(result, pydantic.BaseModel): # if the result is already a Schema, just return it return self.api.create_response( request, From 5a3d4f372ea6b8f5605752817e09436dcde05349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=BC=C3=9Flein?= Date: Tue, 25 Nov 2025 22:11:01 +0000 Subject: [PATCH 3/4] fix a weird ruff lint situation, add a small test --- ninja/operation.py | 24 ++++++++++++------------ tests/test_response_params.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/ninja/operation.py b/ninja/operation.py index 48faa0fb6..4bcf31308 100644 --- a/ninja/operation.py +++ b/ninja/operation.py @@ -82,9 +82,9 @@ def __init__( self.throttle_objects: List[BaseThrottle] = [] if throttle is not NOT_SET: for th in throttle: # type: ignore - assert isinstance( - th, BaseThrottle - ), "Throttle should be an instance of BaseThrottle" + assert isinstance(th, BaseThrottle), ( + "Throttle should be an instance of BaseThrottle" + ) self.throttle_objects.append(th) self.signature = ViewSignature(self.path, self.view_func) @@ -164,9 +164,9 @@ def set_api_instance(self, api: "NinjaAPI", router: "Router") -> None: if router.throttle != NOT_SET: _t = router.throttle self.throttle_objects = isinstance(_t, BaseThrottle) and [_t] or _t # type: ignore - assert all( - isinstance(th, BaseThrottle) for th in self.throttle_objects - ), "Throttle should be an instance of BaseThrottle" + assert all(isinstance(th, BaseThrottle) for th in self.throttle_objects), ( + "Throttle should be an instance of BaseThrottle" + ) if self.tags is None: if router.tags is not None: @@ -275,12 +275,12 @@ def _result_to_response( # Empty response. return temporal_response - model_dump_kwargs: Dict[str, Any] = dict( - by_alias=self.by_alias, - exclude_unset=self.exclude_unset, - exclude_defaults=self.exclude_defaults, - exclude_none=self.exclude_none, - ) + model_dump_kwargs: Dict[str, Any] = { + "by_alias": self.by_alias, + "exclude_unset": self.exclude_unset, + "exclude_defaults": self.exclude_defaults, + "exclude_none": self.exclude_none, + } if pydantic_version >= [2, 7]: # pydantic added support for serialization context at 2.7 model_dump_kwargs.update( diff --git a/tests/test_response_params.py b/tests/test_response_params.py index 192e16400..98a2accef 100644 --- a/tests/test_response_params.py +++ b/tests/test_response_params.py @@ -34,6 +34,11 @@ def op_exclude_none(request): return {"field1": None, "field2": "default value"} +@api.get("/test-schema-return", response=SomeResponse) +def op_return_schema(request): + return SomeResponse() + + client = TestClient(api) @@ -46,3 +51,8 @@ def test_arguments(): assert client.get("/test-unset").json() == {"field3": 10} assert client.get("/test-defaults").json() == {"field1": 3} assert client.get("/test-none").json() == {"field2": "default value"} + assert client.get("/test-schema-return").json() == { + "field1": 1, + "field2": "default value", + "field3": None, + } From 597f783ec93fc50709d73472fd5cd65182fcdf23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=BC=C3=9Flein?= Date: Tue, 25 Nov 2025 22:16:37 +0000 Subject: [PATCH 4/4] reverting some style changes due to old ruff --- ninja/operation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ninja/operation.py b/ninja/operation.py index 4bcf31308..20a2c583c 100644 --- a/ninja/operation.py +++ b/ninja/operation.py @@ -82,9 +82,9 @@ def __init__( self.throttle_objects: List[BaseThrottle] = [] if throttle is not NOT_SET: for th in throttle: # type: ignore - assert isinstance(th, BaseThrottle), ( - "Throttle should be an instance of BaseThrottle" - ) + assert isinstance( + th, BaseThrottle + ), "Throttle should be an instance of BaseThrottle" self.throttle_objects.append(th) self.signature = ViewSignature(self.path, self.view_func) @@ -164,9 +164,9 @@ def set_api_instance(self, api: "NinjaAPI", router: "Router") -> None: if router.throttle != NOT_SET: _t = router.throttle self.throttle_objects = isinstance(_t, BaseThrottle) and [_t] or _t # type: ignore - assert all(isinstance(th, BaseThrottle) for th in self.throttle_objects), ( - "Throttle should be an instance of BaseThrottle" - ) + assert all( + isinstance(th, BaseThrottle) for th in self.throttle_objects + ), "Throttle should be an instance of BaseThrottle" if self.tags is None: if router.tags is not None: