Repository: vitalik/django-ninja

Issue:
Pass serialization context (similar to validation context) with request to `model_dump`
**Is your feature request related to a problem? Please describe.**
We're trying to post-process response entities to erase data from responses based on user permissions. 

We're using custom framework to do this (somewhat similar to ninja permissions, but applied at field level). Based on model class and user permissions it generates list of fields to exclude to use with `model_dump`.
```
excludes = get_excludes_by_permissions(api_response_schema, request.user)
json_response = api_response_schema.model_dump_json(exclude=excludes)
```

**Describe the solution you'd like**
Pydantic starting from 2.7 added serialization context support (https://github.com/pydantic/pydantic/pull/8965)

Ninja already passes similar context to validation.
`Operation._result_to_response` https://github.com/vitalik/django-ninja/blob/master/ninja/operation.py#L258-L269

It would be useful to have request in serialization context as well.

Discussion:

Patch:
diff --git a/ninja/operation.py b/ninja/operation.py
index b47eb4f..4d6bb64 100644
--- a/ninja/operation.py
+++ b/ninja/operation.py
@@ -21,7 +21,7 @@ from django.http.response import HttpResponseBase
 from ninja.constants import NOT_SET, NOT_SET_TYPE
 from ninja.errors import AuthenticationError, ConfigError, Throttled, ValidationError
 from ninja.params.models import TModels
-from ninja.schema import Schema
+from ninja.schema import Schema, pydantic_version
 from ninja.signature import ViewSignature, is_async
 from ninja.throttling import BaseThrottle
 from ninja.types import DictStrAny
@@ -261,11 +261,19 @@ class Operation:
             resp_object, context={"request": request, "response_status": status}
         )
 
+        model_dump_kwargs = {}
+        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"]
         return self.api.create_response(
             request, result, temporal_response=temporal_response


Test patch:
diff --git a/tests/test_serialization_context.py b/tests/test_serialization_context.py
new file mode 100644
index 0000000..3b24f4c
--- /dev/null
+++ b/tests/test_serialization_context.py
@@ -0,0 +1,75 @@
+from unittest import mock
+
+import pytest
+from pydantic import model_serializer
+
+from ninja import Router, Schema
+from ninja.schema import pydantic_version
+from ninja.testing import TestClient
+
+
+def api_endpoint_test(request):
+    return {
+        "test1": "foo",
+        "test2": "bar",
+    }
+
+
+@pytest.mark.skipif(
+    pydantic_version < [2, 7],
+    reason="Serialization context was introduced in Pydantic 2.7",
+)
+def test_request_is_passed_in_context_when_supported():
+    class SchemaWithCustomSerializer(Schema):
+        test1: str
+        test2: str
+
+        @model_serializer(mode="wrap")
+        def ser_model(self, handler, info):
+            assert "request" in info.context
+            assert info.context["request"].path == "/test"  # check it is HttRequest
+            assert "response_status" in info.context
+
+            return handler(self)
+
+    router = Router()
+    router.add_api_operation(
+        "/test", ["GET"], api_endpoint_test, response=SchemaWithCustomSerializer
+    )
+
+    TestClient(router).get("/test")
+
+
+@pytest.mark.parametrize(
+    ["pydantic_version"],
+    [
+        [[2, 0]],
+        [[2, 4]],
+        [[2, 6]],
+    ],
+)
+def test_no_serialisation_context_used_when_no_supported(pydantic_version):
+    class SchemaWithCustomSerializer(Schema):
+        test1: str
+        test2: str
+
+        @model_serializer(mode="wrap")
+        def ser_model(self, handler, info):
+            if hasattr(info, "context"):
+                # an actually newer Pydantic, but pydantic_version is still mocked, so no context is expected
+                assert info.context is None
+
+            return handler(self)
+
+    with mock.patch("ninja.operation.pydantic_version", pydantic_version):
+        router = Router()
+        router.add_api_operation(
+            "/test", ["GET"], api_endpoint_test, response=SchemaWithCustomSerializer
+        )
+
+        resp_json = TestClient(router).get("/test").json()
+
+        assert resp_json == {
+            "test1": "foo",
+            "test2": "bar",
+        }