Repository: vitalik/django-ninja
Pull request #1134 (2024-04-22 11:00:58)

Update LimitOffsetPagination.Input schema
add `le=` parameter to LimitOffsetPagination.Input so the max input is visible and validated on the schema level.

Current implementation of limit is a little bit misleading - when `NINJA_PAGINATION_MAX_LIMIT` is set to 100 and limit in request is set to 1000 - the request is successful and only returns 100 items - see `LimitOffsetPagination.paginate_queryset`

```python 
        limit: int = min(pagination.limit, settings.PAGINATION_MAX_LIMIT)
```

This is a little bit misleading behaviour and enforcing the limit on schema level can remove some confusion and will directly tell the consumer what the limit actually is.


**Warning - this is a breaking change for integrations which are using higher limit in request than it is set on the server.**


Patch:
diff --git a/ninja/pagination.py b/ninja/pagination.py
index 379b91b..bafa483 100644
--- a/ninja/pagination.py
+++ b/ninja/pagination.py
@@ -1,6 +1,7 @@
 import inspect
 from abc import ABC, abstractmethod
 from functools import partial, wraps
+from math import inf
 from typing import Any, Callable, List, Optional, Tuple, Type
 
 from django.db.models import QuerySet
@@ -55,7 +56,13 @@ class PaginationBase(ABC):
 
 class LimitOffsetPagination(PaginationBase):
     class Input(Schema):
-        limit: int = Field(settings.PAGINATION_PER_PAGE, ge=1)
+        limit: int = Field(
+            settings.PAGINATION_PER_PAGE,
+            ge=1,
+            le=settings.PAGINATION_MAX_LIMIT
+            if settings.PAGINATION_MAX_LIMIT != inf
+            else None,
+        )
         offset: int = Field(0, ge=0)
 
     def paginate_queryset(


Test patch:
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index c132528..4c9cdeb 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -1,6 +1,8 @@
+import importlib
 from typing import Any, List
 
 import pytest
+from django.test import override_settings
 
 from ninja import NinjaAPI, Schema
 from ninja.errors import ConfigError
@@ -324,6 +326,81 @@ def test_case9():
     }
 
 
+@override_settings(NINJA_PAGINATION_MAX_LIMIT=1000)
+def test_10_max_limit_set():
+    # reload to apply django settings
+    from ninja import conf, pagination
+
+    importlib.reload(conf)
+    importlib.reload(pagination)
+    new_api = NinjaAPI()
+    new_client = TestClient(new_api)
+
+    @new_api.get("/items_10", response=List[int])
+    @paginate  # LimitOffsetPagination is set as default
+    def items_10(request, **kwargs):
+        return ITEMS
+
+    response = new_client.get("/items_10?limit=1000").json()
+    assert response == {"items": ITEMS[:1000], "count": 100}
+
+    schema = new_api.get_openapi_schema()["paths"]["/api/items_10"]["get"]
+    # print(schema)
+    assert schema["parameters"] == [
+        {
+            "in": "query",
+            "name": "limit",
+            "schema": {
+                "title": "Limit",
+                "default": 100,
+                "minimum": 1,
+                "maximum": 1000,
+                "type": "integer",
+            },
+            "required": False,
+        },
+        {
+            "in": "query",
+            "name": "offset",
+            "schema": {
+                "title": "Offset",
+                "default": 0,
+                "minimum": 0,
+                "type": "integer",
+            },
+            "required": False,
+        },
+    ]
+
+
+@override_settings(NINJA_PAGINATION_MAX_LIMIT=1000)
+def test_11_max_limit_set_and_exceeded():
+    # reload to apply django settings
+    from ninja import conf, pagination
+
+    importlib.reload(conf)
+    importlib.reload(pagination)
+    new_api = NinjaAPI()
+    new_client = TestClient(new_api)
+
+    @new_api.get("/items_11", response=List[int])
+    @paginate  # LimitOffsetPagination is set as default
+    def items_11(request, **kwargs):
+        return ITEMS
+
+    response = new_client.get("/items_11?limit=1001").json()
+    assert response == {
+        "detail": [
+            {
+                "ctx": {"le": 1000},
+                "loc": ["query", "limit"],
+                "msg": "Input should be less than or equal to 1000",
+                "type": "less_than_equal",
+            }
+        ]
+    }
+
+
 def test_config_error_None():
     with pytest.raises(ConfigError):