django-ninja-aio-crud 2.7.0__tar.gz → 2.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-ninja-aio-crud might be problematic. Click here for more details.

Files changed (104) hide show
  1. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/docs.yml +4 -0
  2. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/model_serializer.md +71 -7
  4. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/model_util.md +44 -16
  5. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/models/serializers.md +40 -3
  6. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/api_view_set.md +80 -20
  7. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/__init__.py +1 -1
  8. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/api.py +1 -1
  9. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/query.py +3 -0
  10. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/serializers.py +153 -70
  11. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/utils.py +115 -64
  12. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/helpers.py +34 -6
  13. django_ninja_aio_crud-2.9.0/ninja_aio/types.py +24 -0
  14. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/api.py +26 -9
  15. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/models.py +7 -7
  16. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/views.py +1 -1
  17. django_ninja_aio_crud-2.9.0/tests/helpers/test_many_to_many_api.py +282 -0
  18. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/models/test_models_extra.py +164 -6
  19. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/models.py +13 -0
  20. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_serializers.py +127 -0
  21. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/views/test_viewset.py +143 -0
  22. django_ninja_aio_crud-2.7.0/ninja_aio/types.py +0 -19
  23. django_ninja_aio_crud-2.7.0/tests/helpers/test_many_to_many_api.py +0 -118
  24. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/dependabot.yml +0 -0
  25. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/coverage.yml +0 -0
  26. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.github/workflows/publish.yml +0 -0
  27. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.gitignore +0 -0
  28. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/.pre-commit-config.yaml +0 -0
  29. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/LICENSE +0 -0
  30. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/README.md +0 -0
  31. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/CNAME +0 -0
  32. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/authentication.md +0 -0
  33. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/pagination.md +0 -0
  34. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/renderers/orjson_renderer.md +0 -0
  35. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/api_view.md +0 -0
  36. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/decorators.md +0 -0
  37. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/api/views/mixins.md +0 -0
  38. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/auth.md +0 -0
  39. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/contributing.md +0 -0
  40. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/extra.css +0 -0
  41. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  42. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  43. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  44. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  45. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  46. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  47. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/installation.md +0 -0
  48. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/getting_started/quick_start.md +0 -0
  49. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/bar-swagger.png +0 -0
  50. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/favicon.ico +0 -0
  51. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/foo-swagger.png +0 -0
  52. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/logo.png +0 -0
  53. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  54. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/index.md +0 -0
  55. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/release_notes.md +0 -0
  56. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/requirements.txt +0 -0
  57. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/authentication.md +0 -0
  58. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/crud.md +0 -0
  59. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/filtering.md +0 -0
  60. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/docs/tutorial/model.md +0 -0
  61. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/main.py +0 -0
  62. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/mkdocs.yml +0 -0
  63. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/api.py +0 -0
  64. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/auth.py +0 -0
  65. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/__init__.py +0 -0
  66. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/operations.py +0 -0
  67. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/decorators/views.py +0 -0
  68. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/exceptions.py +0 -0
  69. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/factory/__init__.py +0 -0
  70. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/factory/operations.py +0 -0
  71. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/helpers/__init__.py +0 -0
  72. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/models/__init__.py +0 -0
  73. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/parsers.py +0 -0
  74. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/renders.py +0 -0
  75. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/__init__.py +0 -0
  76. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/api.py +0 -0
  77. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/schemas/generics.py +0 -0
  78. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/__init__.py +0 -0
  79. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/ninja_aio/views/mixins.py +0 -0
  80. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/pyproject.toml +0 -0
  81. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/requirements.dev.txt +0 -0
  82. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/run-local-coverage.sh +0 -0
  83. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/__init__.py +0 -0
  84. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/__init__.py +0 -0
  85. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_decorators.py +0 -0
  86. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_exceptions_api.py +0 -0
  87. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/core/test_renderer_parser.py +0 -0
  88. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/__init__.py +0 -0
  89. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/literals.py +0 -0
  90. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/generics/request.py +0 -0
  91. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/helpers/__init__.py +0 -0
  92. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/models/__init__.py +0 -0
  93. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/models/test_model_util.py +0 -0
  94. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/__init__.py +0 -0
  95. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/schema.py +0 -0
  96. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/serializers.py +0 -0
  97. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_app/views.py +0 -0
  98. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_auth.py +0 -0
  99. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_decorators.py +0 -0
  100. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_exceptions.py +0 -0
  101. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_query_util.py +0 -0
  102. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/test_settings.py +0 -0
  103. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/views/__init__.py +0 -0
  104. {django_ninja_aio_crud-2.7.0 → django_ninja_aio_crud-2.9.0}/tests/views/test_views.py +0 -0
@@ -19,6 +19,8 @@ on:
19
19
  - "2.5"
20
20
  - "2.6"
21
21
  - "2.6.1"
22
+ - "2.7.0"
23
+ - "2.8"
22
24
  make_latest:
23
25
  description: 'Set as "latest" and default?'
24
26
  type: boolean
@@ -41,6 +43,8 @@ on:
41
43
  - "2.5"
42
44
  - "2.6"
43
45
  - "2.6.1"
46
+ - "2.7.0"
47
+ - "2.8"
44
48
  delete_confirm:
45
49
  description: 'Confirm deletion of the selected version'
46
50
  type: boolean
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.7.0
3
+ Version: 2.9.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -145,6 +145,68 @@ class User(ModelSerializer):
145
145
  }
146
146
  ```
147
147
 
148
+ ### DetailSerializer
149
+
150
+ Describes how to build a detail (single object) output schema. Use this when you want the retrieve endpoint to return more fields than the list endpoint.
151
+
152
+ **Fallback Behavior:** If `DetailSerializer` is not defined, `generate_detail_s()` automatically falls back to the read schema (same as `generate_read_s()`). This means you only need to define `DetailSerializer` when you want different fields for single-object retrieval vs list views.
153
+
154
+ **Attributes:**
155
+
156
+ | Attribute | Type | Description |
157
+ | ----------- | ------------------------ | ----------------------------------------------------------------------------- |
158
+ | `fields` | `list[str]` | Model fields to include in detail view (falls back to ReadSerializer fields if not defined) |
159
+ | `excludes` | `list[str]` | Fields to exclude from detail view |
160
+ | `customs` | `list[tuple]` | Computed fields: `(name, type)` required; `(name, type, default)` optional |
161
+ | `optionals` | `list[tuple[str, type]]` | Optional output fields |
162
+
163
+ **Example:**
164
+
165
+ ```python
166
+ class Article(ModelSerializer):
167
+ title = models.CharField(max_length=200)
168
+ summary = models.TextField()
169
+ content = models.TextField()
170
+ author = models.ForeignKey(User, on_delete=models.CASCADE)
171
+ tags = models.ManyToManyField(Tag)
172
+ view_count = models.IntegerField(default=0)
173
+
174
+ class ReadSerializer:
175
+ # List view: minimal fields for performance
176
+ fields = ["id", "title", "summary", "author"]
177
+
178
+ class DetailSerializer:
179
+ # Detail view: all fields including expensive relations
180
+ fields = ["id", "title", "summary", "content", "author", "tags", "view_count"]
181
+ customs = [
182
+ ("reading_time", int, lambda obj: len(obj.content.split()) // 200),
183
+ ]
184
+ ```
185
+
186
+ **Generated Output (List):**
187
+
188
+ ```json
189
+ [
190
+ {"id": 1, "title": "Getting Started", "summary": "...", "author": {...}},
191
+ {"id": 2, "title": "Advanced Topics", "summary": "...", "author": {...}}
192
+ ]
193
+ ```
194
+
195
+ **Generated Output (Detail):**
196
+
197
+ ```json
198
+ {
199
+ "id": 1,
200
+ "title": "Getting Started",
201
+ "summary": "...",
202
+ "content": "Full article content here...",
203
+ "author": {...},
204
+ "tags": [{"id": 1, "name": "python"}, {"id": 2, "name": "django"}],
205
+ "view_count": 1234,
206
+ "reading_time": 5
207
+ }
208
+ ```
209
+
148
210
  ### UpdateSerializer
149
211
 
150
212
  Describes how to build an update (partial/full) input schema.
@@ -194,14 +256,15 @@ class User(ModelSerializer):
194
256
 
195
257
  ### Auto-Generated Schemas
196
258
 
197
- ModelSerializer automatically generates four schema types:
259
+ ModelSerializer automatically generates five schema types:
198
260
 
199
- | Method | Schema Type | Purpose |
200
- | -------------------------- | ------------------ | ------------------------------ |
201
- | `generate_create_s()` | Input ("In") | POST endpoint payload |
202
- | `generate_update_s()` | Input ("Patch") | PATCH/PUT endpoint payload |
203
- | `generate_read_s(depth=1)` | Output ("Out") | Response with nested relations |
204
- | `generate_related_s()` | Output ("Related") | Compact nested representation |
261
+ | Method | Schema Type | Purpose |
262
+ | ---------------------------- | ------------------ | ------------------------------------ |
263
+ | `generate_create_s()` | Input ("In") | POST endpoint payload |
264
+ | `generate_update_s()` | Input ("Patch") | PATCH/PUT endpoint payload |
265
+ | `generate_read_s(depth=1)` | Output ("Out") | List response with nested relations |
266
+ | `generate_detail_s(depth=1)` | Output ("Detail") | Single object response (retrieve) |
267
+ | `generate_related_s()` | Output ("Related") | Compact nested representation |
205
268
 
206
269
  **Example:**
207
270
 
@@ -219,6 +282,7 @@ class User(ModelSerializer):
219
282
  # Auto-generate schemas
220
283
  UserCreateSchema = User.generate_create_s()
221
284
  UserReadSchema = User.generate_read_s()
285
+ UserDetailSchema = User.generate_detail_s() # Falls back to read schema if DetailSerializer not defined
222
286
  UserUpdateSchema = User.generate_update_s()
223
287
  UserRelatedSchema = User.generate_related_s()
224
288
  ```
@@ -68,7 +68,7 @@ print(util.model_fields)
68
68
 
69
69
  ### `serializable_fields`
70
70
 
71
- Returns serializable fields (ReadSerializer fields or all model fields).
71
+ Returns serializable fields for read operations (ReadSerializer fields or all model fields).
72
72
 
73
73
  ```python
74
74
  class User(ModelSerializer):
@@ -84,6 +84,27 @@ print(util.serializable_fields)
84
84
  # ["id", "username", "email"] (password excluded)
85
85
  ```
86
86
 
87
+ ### `serializable_detail_fields`
88
+
89
+ Returns serializable fields for detail operations (DetailSerializer fields, or falls back to ReadSerializer fields).
90
+
91
+ ```python
92
+ class Article(ModelSerializer):
93
+ title = models.CharField(max_length=200)
94
+ summary = models.TextField()
95
+ content = models.TextField()
96
+
97
+ class ReadSerializer:
98
+ fields = ["id", "title", "summary"]
99
+
100
+ class DetailSerializer:
101
+ fields = ["id", "title", "summary", "content"]
102
+
103
+ util = ModelUtil(Article)
104
+ print(util.serializable_fields) # ["id", "title", "summary"]
105
+ print(util.serializable_detail_fields) # ["id", "title", "summary", "content"]
106
+ ```
107
+
87
108
  ### `model_name`
88
109
 
89
110
  Returns the Django internal model name.
@@ -118,6 +139,10 @@ class Book(ModelSerializer):
118
139
  select_related=["author", "category"],
119
140
  prefetch_related=["tags"],
120
141
  )
142
+ detail = ModelQuerySetSchema(
143
+ select_related=["author", "category", "publisher"],
144
+ prefetch_related=["tags", "reviews"],
145
+ )
121
146
  queryset_request = ModelQuerySetSchema(
122
147
  select_related=[],
123
148
  prefetch_related=["related_items"],
@@ -131,9 +156,10 @@ class Book(ModelSerializer):
131
156
  ]
132
157
  ```
133
158
 
134
- - read: applied to read operations (list/retrieve).
135
- - queryset_request: applied inside queryset_request hook.
136
- - extras: named configurations available via QueryUtil.SCOPES.
159
+ - **read**: applied to list operations (`is_for="read"`).
160
+ - **detail**: applied to retrieve operations (`is_for="detail"`). Falls back to `read` if not defined.
161
+ - **queryset_request**: applied inside queryset_request hook.
162
+ - **extras**: named configurations available via QueryUtil.SCOPES.
137
163
 
138
164
  ## QueryUtil
139
165
 
@@ -178,7 +204,7 @@ qs = await ModelUtil(Book).get_objects(
178
204
  prefetch_related=["tags"],
179
205
  ),
180
206
  with_qs_request=True, # Apply queryset_request hook
181
- is_for_read=True, # union with auto-discovered relations
207
+ is_for="read", # union with auto-discovered relations for read
182
208
  )
183
209
  ```
184
210
 
@@ -187,7 +213,7 @@ qs = await ModelUtil(Book).get_objects(
187
213
  - `request` (`HttpRequest`): Current HTTP request
188
214
  - `query_data` (`ObjectsQuerySchema | None`): Query configuration (filters, select_related, prefetch_related)
189
215
  - `with_qs_request` (`bool`): Apply queryset_request hook if available (default: True)
190
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
216
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query, determines which serializable fields to use for optimization. Use `"read"` for list operations, `"detail"` for retrieve operations. If `None`, only query_data optimizations are applied. (default: None)
191
217
 
192
218
  **Returns:** Optimized `QuerySet`
193
219
 
@@ -198,13 +224,13 @@ Fetch a single object by pk or getters with optimizations:
198
224
  ```python
199
225
  from ninja_aio.schemas.helpers import ObjectQuerySchema, QuerySchema
200
226
 
201
- # by pk + select/prefetch
227
+ # by pk + select/prefetch for detail view
202
228
  obj = await ModelUtil(Book).get_object(
203
229
  request,
204
230
  pk=42,
205
231
  query_data=ObjectQuerySchema(select_related=["author"]),
206
232
  with_qs_request=True,
207
- is_for_read=True,
233
+ is_for="detail",
208
234
  )
209
235
 
210
236
  # by getters (required if pk omitted)
@@ -220,7 +246,7 @@ obj = await ModelUtil(Book).get_object(
220
246
  - `pk` (`int | str | None`): Primary key value (optional if getters provided)
221
247
  - `query_data` (`ObjectQuerySchema | QuerySchema | None`): Query configuration
222
248
  - `with_qs_request` (`bool`): Apply queryset_request hook if available (default: True)
223
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
249
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query. Use `"detail"` for single object retrieval, `"read"` for list operations. (default: None)
224
250
 
225
251
  **Returns:** Model instance
226
252
 
@@ -235,16 +261,17 @@ Uniform serialization methods that accept either instances or query data:
235
261
 
236
262
  ```python
237
263
  schema = Book.generate_read_s()
264
+ detail_schema = Book.generate_detail_s()
238
265
 
239
266
  # single instance
240
267
  data = await ModelUtil(Book).read_s(schema, request, instance=obj)
241
268
 
242
- # single via getters
269
+ # single via getters (detail view)
243
270
  data = await ModelUtil(Book).read_s(
244
- schema,
271
+ detail_schema,
245
272
  request,
246
273
  query_data=ObjectQuerySchema(getters={"pk": 42}),
247
- is_for_read=True,
274
+ is_for="detail",
248
275
  )
249
276
 
250
277
  # list from queryset
@@ -255,7 +282,7 @@ items = await ModelUtil(Book).list_read_s(
255
282
  schema,
256
283
  request,
257
284
  query_data=ObjectsQuerySchema(filters={"is_published": True}),
258
- is_for_read=True,
285
+ is_for="read",
259
286
  )
260
287
  ```
261
288
 
@@ -265,7 +292,7 @@ items = await ModelUtil(Book).list_read_s(
265
292
  - `request` (`HttpRequest`): Current HTTP request
266
293
  - `instance` (`Model | None`): Model instance to serialize (optional)
267
294
  - `query_data` (`ObjectQuerySchema | QuerySchema | None`): Query configuration for fetching (optional)
268
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
295
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query. Use `"detail"` for single object views, `"read"` for list views. (default: None)
269
296
 
270
297
  **Parameters (list_read_s):**
271
298
 
@@ -273,11 +300,12 @@ items = await ModelUtil(Book).list_read_s(
273
300
  - `request` (`HttpRequest`): Current HTTP request
274
301
  - `instances` (`QuerySet | list[Model] | None`): Instances to serialize (optional)
275
302
  - `query_data` (`ObjectsQuerySchema | None`): Query configuration for fetching (optional)
276
- - `is_for_read` (`bool`): Merge with read-specific optimizations (default: False)
303
+ - `is_for` (`Literal["read", "detail"] | None`): Purpose of the query. Typically `"read"` for list views. (default: None)
277
304
 
278
305
  **Behavior:**
279
306
 
280
- - When `is_for_read=True`, select_related and prefetch_related are merged with model-discovered relations
307
+ - When `is_for` is specified, select_related and prefetch_related are merged with model-discovered relations based on the operation type
308
+ - When `is_for="detail"` but no `QuerySet.detail` is configured, falls back to `QuerySet.read` optimizations
281
309
  - Passing `instance`/`instances` skips fetching; passing `query_data` fetches automatically
282
310
  - Either `instance`/`instances` OR `query_data` must be provided, not both
283
311
 
@@ -37,7 +37,8 @@ Define a Serializer subclass with a nested Meta:
37
37
 
38
38
  - **model**: Django model class
39
39
  - **schema_in**: SchemaModelConfig for create inputs
40
- - **schema_out**: SchemaModelConfig for read outputs
40
+ - **schema_out**: SchemaModelConfig for read outputs (list endpoint)
41
+ - **schema_detail**: SchemaModelConfig for detail outputs (retrieve endpoint)
41
42
  - **schema_update**: SchemaModelConfig for patch/update inputs
42
43
  - **relations_serializers**: Mapping of relation field name -> Serializer class, **string reference**, or **Union of serializers** (supports forward/circular dependencies and polymorphic relations)
43
44
 
@@ -55,13 +56,37 @@ Generate schemas explicitly using these methods:
55
56
  ```python
56
57
  # Explicitly generate schemas when needed
57
58
  ArticleSerializer.generate_create_s() # Returns create (In) schema
58
- ArticleSerializer.generate_read_s() # Returns read (Out) schema
59
+ ArticleSerializer.generate_read_s() # Returns read (Out) schema for list endpoint
60
+ ArticleSerializer.generate_detail_s() # Returns detail (Out) schema for retrieve endpoint
59
61
  ArticleSerializer.generate_update_s() # Returns update (Patch) schema
60
62
  ArticleSerializer.generate_related_s() # Returns related (nested) schema
61
63
  ```
62
64
 
63
65
  Schemas support **forward references and circular dependencies** via string references in `relations_serializers`.
64
66
 
67
+ ### Detail Schema for Retrieve Endpoint
68
+
69
+ Use `schema_detail` when you want the retrieve endpoint to return more fields than the list endpoint:
70
+
71
+ ```python
72
+ class ArticleSerializer(serializers.Serializer):
73
+ class Meta:
74
+ model = models.Article
75
+ schema_out = serializers.SchemaModelConfig(
76
+ # List view: minimal fields for performance
77
+ fields=["id", "title", "summary"]
78
+ )
79
+ schema_detail = serializers.SchemaModelConfig(
80
+ # Detail view: all fields including expensive relations
81
+ fields=["id", "title", "summary", "content", "author", "tags"],
82
+ customs=[("reading_time", int, lambda obj: len(obj.content.split()) // 200)]
83
+ )
84
+ ```
85
+
86
+ When used with `APIViewSet`:
87
+ - **List endpoint** (`GET /articles/`) uses `schema_out`
88
+ - **Retrieve endpoint** (`GET /articles/{pk}`) uses `schema_detail` (falls back to `schema_out` if not defined)
89
+
65
90
  ## Example: simple FK
66
91
 
67
92
  ```python
@@ -450,12 +475,19 @@ class ArticleSerializer(serializers.Serializer):
450
475
  schema_out = serializers.SchemaModelConfig(
451
476
  fields=["id", "title", "content", "author", "category"]
452
477
  )
478
+ schema_detail = serializers.SchemaModelConfig(
479
+ fields=["id", "title", "content", "author", "category", "tags", "comments"]
480
+ )
453
481
 
454
482
  class QuerySet:
455
483
  read = ModelQuerySetSchema(
456
484
  select_related=["author", "category"],
457
485
  prefetch_related=["tags"],
458
486
  )
487
+ detail = ModelQuerySetSchema(
488
+ select_related=["author", "category", "author__profile"],
489
+ prefetch_related=["tags", "comments", "comments__author"],
490
+ )
459
491
  queryset_request = ModelQuerySetSchema(
460
492
  select_related=[],
461
493
  prefetch_related=["comments"],
@@ -469,7 +501,12 @@ class ArticleSerializer(serializers.Serializer):
469
501
  ]
470
502
  ```
471
503
 
472
- The QuerySet configuration is used by ModelUtil to automatically optimize database queries during read operations.
504
+ The QuerySet configuration is used by ModelUtil to automatically optimize database queries:
505
+
506
+ - **read**: Applied to list operations (`is_for="read"`)
507
+ - **detail**: Applied to retrieve/detail operations (`is_for="detail"`). Falls back to `read` if not defined.
508
+ - **queryset_request**: Applied inside the `queryset_request` hook
509
+ - **extras**: Named configurations available via `QueryUtil.SCOPES`
473
510
 
474
511
  ## Complete Example
475
512
 
@@ -4,13 +4,13 @@
4
4
 
5
5
  ## Generated CRUD Endpoints
6
6
 
7
- | Method | Path | Summary | Response |
8
- | ------ | --------------- | -------------- | ---------------------------------- |
9
- | POST | `/{base}/` | Create Model | `201 schema_out` |
10
- | GET | `/{base}/` | List Models | `200 List[schema_out]` (paginated) |
11
- | GET | `/{base}/{pk}` | Retrieve Model | `200 schema_out` |
12
- | PATCH | `/{base}/{pk}/` | Update Model | `200 schema_out` |
13
- | DELETE | `/{base}/{pk}/` | Delete Model | `204 No Content` |
7
+ | Method | Path | Summary | Response |
8
+ | ------ | --------------- | -------------- | --------------------------------------------- |
9
+ | POST | `/{base}/` | Create Model | `201 schema_out` |
10
+ | GET | `/{base}/` | List Models | `200 List[schema_out]` (paginated) |
11
+ | GET | `/{base}/{pk}` | Retrieve Model | `200 schema_detail` (or `schema_out` if none) |
12
+ | PATCH | `/{base}/{pk}/` | Update Model | `200 schema_out` |
13
+ | DELETE | `/{base}/{pk}/` | Delete Model | `204 No Content` |
14
14
 
15
15
  Notes:
16
16
 
@@ -100,7 +100,8 @@ class ArticleViewSet(APIViewSet):
100
100
  | `api` | `NinjaAPI` | — | API instance (required) |
101
101
  | `serializer_class` | `Serializer \| None` | `None` | Serializer class for plain models (alternative to ModelSerializer) |
102
102
  | `schema_in` | `Schema \| None` | `None` (auto) | Create input schema override |
103
- | `schema_out` | `Schema \| None` | `None` (auto) | Read/output schema override |
103
+ | `schema_out` | `Schema \| None` | `None` (auto) | List/output schema override |
104
+ | `schema_detail` | `Schema \| None` | `None` (auto) | Retrieve/detail schema override (falls back to `schema_out`) |
104
105
  | `schema_update` | `Schema \| None` | `None` (auto) | Update input schema override |
105
106
  | `pagination_class` | `type[AsyncPaginationBase]` | `PageNumberPagination` | Pagination strategy |
106
107
  | `query_params` | `dict[str, tuple[type, ...]]` | `{}` | List endpoint filters definition |
@@ -150,6 +151,7 @@ The transaction behavior is applied by default. Custom decorators can be added v
150
151
  If `model` is a subclass of `ModelSerializerMeta`:
151
152
 
152
153
  - `schema_out` is generated from `ReadSerializer`
154
+ - `schema_detail` is generated from `DetailSerializer` (optional, falls back to `schema_out`)
153
155
  - `schema_in` from `CreateSerializer`
154
156
  - `schema_update` from `UpdateSerializer`
155
157
 
@@ -173,7 +175,42 @@ class ArticleViewSet(APIViewSet):
173
175
  serializer_class = ArticleSerializer
174
176
  ```
175
177
 
176
- Otherwise provide schemas manually via `schema_in`, `schema_out`, and `schema_update` attributes.
178
+ Otherwise provide schemas manually via `schema_in`, `schema_out`, `schema_detail`, and `schema_update` attributes.
179
+
180
+ ### Detail Schema for Retrieve Endpoint
181
+
182
+ Use `schema_detail` (or `DetailSerializer` on ModelSerializer) when you want the retrieve endpoint to return more fields than the list endpoint. This is useful for:
183
+
184
+ - **Performance optimization**: List endpoints return minimal fields, retrieve endpoints include expensive relations
185
+ - **API design**: Clients get a summary in lists and full details on individual requests
186
+
187
+ ```python
188
+ from ninja_aio.models import ModelSerializer
189
+ from django.db import models
190
+
191
+ class Article(ModelSerializer):
192
+ title = models.CharField(max_length=200)
193
+ summary = models.TextField()
194
+ content = models.TextField()
195
+ author = models.ForeignKey(User, on_delete=models.CASCADE)
196
+ tags = models.ManyToManyField(Tag)
197
+
198
+ class ReadSerializer:
199
+ # List view: minimal fields
200
+ fields = ["id", "title", "summary"]
201
+
202
+ class DetailSerializer:
203
+ # Detail view: all fields
204
+ fields = ["id", "title", "summary", "content", "author", "tags"]
205
+
206
+ @api.viewset(model=Article)
207
+ class ArticleViewSet(APIViewSet):
208
+ pass # Schemas auto-generated from model
209
+ ```
210
+
211
+ Endpoints behavior:
212
+ - `GET /articles/` returns `[{"id": 1, "title": "...", "summary": "..."}, ...]`
213
+ - `GET /articles/1` returns `{"id": 1, "title": "...", "summary": "...", "content": "...", "author": {...}, "tags": [...]}`
177
214
 
178
215
  ## List Filtering
179
216
 
@@ -254,6 +291,7 @@ Relations are declared via `M2MRelationSchema` objects (not tuples). Each schema
254
291
  - `get`: enable GET listing (bool)
255
292
  - `filters`: dict of `{param_name: (type, default)}` for relation-level filtering
256
293
  - `related_schema`: optional pre-built schema for the related model (auto-generated if the `model` is a `ModelSerializer`)
294
+ - `serializer_class`: optional `Serializer` class for plain Django models. When provided, `related_schema` is auto-generated from the serializer. Cannot be used when `model` is a `ModelSerializer`.
257
295
  - `append_slash`: bool to control trailing slash for the GET relation endpoint path. Defaults to `False` (no trailing slash) for backward compatibility. When `True`, the GET path ends with a trailing slash.
258
296
 
259
297
  If `path` is empty it falls back to the related model verbose name (lowercase plural).
@@ -285,21 +323,43 @@ async def tags_query_params_handler(self, queryset, filters_dict):
285
323
 
286
324
  Warning: Model support
287
325
 
288
- - You can supply a standard Django `Model` (not a `ModelSerializer`) in `M2MRelationSchema.model`. When doing so you must provide `related_schema` manually:
326
+ - You can supply a standard Django `Model` (not a `ModelSerializer`) in `M2MRelationSchema.model`. When doing so you must provide either `related_schema` manually or `serializer_class`:
289
327
 
290
- ```python
291
- M2MRelationSchema(
292
- model=Tag, # plain django.db.models.Model
293
- related_name="tags",
294
- related_schema=TagOut, # a Pydantic/Ninja Schema you define
295
- add=True,
296
- remove=True,
297
- get=True,
298
- )
299
- ```
328
+ === "With related_schema"
329
+ ```python
330
+ M2MRelationSchema(
331
+ model=Tag, # plain django.db.models.Model
332
+ related_name="tags",
333
+ related_schema=TagOut, # a Pydantic/Ninja Schema you define
334
+ add=True,
335
+ remove=True,
336
+ get=True,
337
+ )
338
+ ```
339
+
340
+ === "With serializer_class"
341
+ ```python
342
+ from ninja_aio.models import serializers
343
+
344
+ class TagSerializer(serializers.Serializer):
345
+ class Meta:
346
+ model = Tag
347
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
348
+
349
+ M2MRelationSchema(
350
+ model=Tag, # plain django.db.models.Model
351
+ related_name="tags",
352
+ serializer_class=TagSerializer, # auto-generates related_schema
353
+ add=True,
354
+ remove=True,
355
+ get=True,
356
+ )
357
+ ```
300
358
 
301
359
  For `ModelSerializer` models, `related_schema` can be inferred automatically (via internal helpers).
302
360
 
361
+ Note: You cannot use `serializer_class` when `model` is already a `ModelSerializer` - this will raise a `ValueError`.
362
+
303
363
  Example with filters:
304
364
 
305
365
  ```python
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.7.0"
3
+ __version__ = "2.9.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -472,7 +472,7 @@ class ManyToManyAPI:
472
472
  model = relation.model
473
473
  related_name = relation.related_name
474
474
  m2m_auth = relation.auth or self.default_auth
475
- rel_util = ModelUtil(model)
475
+ rel_util = ModelUtil(model, serializer_class=relation.serializer_class)
476
476
  rel_path = relation.path or rel_util.verbose_name_path_resolver()
477
477
  related_schema = relation.related_schema
478
478
  m2m_add, m2m_remove, m2m_get = relation.add, relation.remove, relation.get
@@ -75,6 +75,9 @@ class QueryUtil:
75
75
  self.queryset_request_config: ModelQuerySetSchema = self._configs.get(
76
76
  self.SCOPES.QUERYSET_REQUEST, ModelQuerySetSchema()
77
77
  )
78
+ self.detail_config: ModelQuerySetSchema = self._configs.get(
79
+ self.SCOPES.DETAIL, ModelQuerySetSchema()
80
+ )
78
81
 
79
82
  def _get_config(self, conf_name: str) -> ModelQuerySetSchema:
80
83
  """Helper method to retrieve configuration attributes."""