django-ninja-aio-crud 2.6.1__tar.gz → 2.8.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.
Files changed (105) hide show
  1. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/workflows/docs.yml +4 -0
  2. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/models/model_serializer.md +69 -7
  4. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/models/serializers.md +206 -5
  5. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/api_view_set.md +80 -20
  6. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/__init__.py +1 -1
  7. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/helpers/api.py +1 -1
  8. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/models/serializers.py +281 -89
  9. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/helpers.py +32 -6
  10. django_ninja_aio_crud-2.8.0/ninja_aio/types.py +24 -0
  11. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/views/api.py +21 -7
  12. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/views.py +1 -1
  13. django_ninja_aio_crud-2.8.0/tests/helpers/test_many_to_many_api.py +282 -0
  14. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/models.py +13 -0
  15. django_ninja_aio_crud-2.8.0/tests/test_serializers.py +412 -0
  16. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/views/test_viewset.py +141 -0
  17. django_ninja_aio_crud-2.6.1/ninja_aio/types.py +0 -19
  18. django_ninja_aio_crud-2.6.1/tests/helpers/test_many_to_many_api.py +0 -118
  19. django_ninja_aio_crud-2.6.1/tests/test_serializers.py +0 -106
  20. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/dependabot.yml +0 -0
  21. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/workflows/coverage.yml +0 -0
  22. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/workflows/publish.yml +0 -0
  23. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.gitignore +0 -0
  24. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.pre-commit-config.yaml +0 -0
  25. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/LICENSE +0 -0
  26. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/README.md +0 -0
  27. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/CNAME +0 -0
  28. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/authentication.md +0 -0
  29. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/models/model_util.md +0 -0
  30. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/pagination.md +0 -0
  31. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/renderers/orjson_renderer.md +0 -0
  32. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/api_view.md +0 -0
  33. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/decorators.md +0 -0
  34. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/mixins.md +0 -0
  35. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/auth.md +0 -0
  36. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/contributing.md +0 -0
  37. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/extra.css +0 -0
  38. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  39. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  40. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  41. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  42. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  43. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  44. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/installation.md +0 -0
  45. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/quick_start.md +0 -0
  46. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/bar-swagger.png +0 -0
  47. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/favicon.ico +0 -0
  48. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/foo-swagger.png +0 -0
  49. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/logo.png +0 -0
  50. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  51. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/index.md +0 -0
  52. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/release_notes.md +0 -0
  53. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/requirements.txt +0 -0
  54. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/authentication.md +0 -0
  55. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/crud.md +0 -0
  56. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/filtering.md +0 -0
  57. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/model.md +0 -0
  58. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/main.py +0 -0
  59. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/mkdocs.yml +0 -0
  60. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/api.py +0 -0
  61. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/auth.py +0 -0
  62. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/decorators/__init__.py +0 -0
  63. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/decorators/operations.py +0 -0
  64. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/decorators/views.py +0 -0
  65. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/exceptions.py +0 -0
  66. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/factory/__init__.py +0 -0
  67. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/factory/operations.py +0 -0
  68. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/helpers/__init__.py +0 -0
  69. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/helpers/query.py +0 -0
  70. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/models/__init__.py +0 -0
  71. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/models/utils.py +0 -0
  72. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/parsers.py +0 -0
  73. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/renders.py +0 -0
  74. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/__init__.py +0 -0
  75. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/api.py +0 -0
  76. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/generics.py +0 -0
  77. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/views/__init__.py +0 -0
  78. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/views/mixins.py +0 -0
  79. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/pyproject.toml +0 -0
  80. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/requirements.dev.txt +0 -0
  81. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/run-local-coverage.sh +0 -0
  82. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/__init__.py +0 -0
  83. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/__init__.py +0 -0
  84. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/test_decorators.py +0 -0
  85. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/test_exceptions_api.py +0 -0
  86. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/test_renderer_parser.py +0 -0
  87. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/__init__.py +0 -0
  88. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/literals.py +0 -0
  89. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/models.py +0 -0
  90. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/request.py +0 -0
  91. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/helpers/__init__.py +0 -0
  92. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/models/__init__.py +0 -0
  93. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/models/test_model_util.py +0 -0
  94. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/models/test_models_extra.py +0 -0
  95. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/__init__.py +0 -0
  96. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/schema.py +0 -0
  97. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/serializers.py +0 -0
  98. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/views.py +0 -0
  99. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_auth.py +0 -0
  100. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_decorators.py +0 -0
  101. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_exceptions.py +0 -0
  102. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_query_util.py +0 -0
  103. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_settings.py +0 -0
  104. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/views/__init__.py +0 -0
  105. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/views/test_views.py +0 -0
@@ -18,6 +18,8 @@ on:
18
18
  - "2.4"
19
19
  - "2.5"
20
20
  - "2.6"
21
+ - "2.6.1"
22
+ - "2.7.0"
21
23
  make_latest:
22
24
  description: 'Set as "latest" and default?'
23
25
  type: boolean
@@ -39,6 +41,8 @@ on:
39
41
  - "2.4"
40
42
  - "2.5"
41
43
  - "2.6"
44
+ - "2.6.1"
45
+ - "2.7.0"
42
46
  delete_confirm:
43
47
  description: 'Confirm deletion of the selected version'
44
48
  type: boolean
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.6.1
3
+ Version: 2.8.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,66 @@ 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
+ **Attributes:**
153
+
154
+ | Attribute | Type | Description |
155
+ | ----------- | ------------------------ | ----------------------------------------------------------------------------- |
156
+ | `fields` | `list[str]` | Model fields to include in detail view |
157
+ | `excludes` | `list[str]` | Fields to exclude from detail view |
158
+ | `customs` | `list[tuple]` | Computed fields: `(name, type)` required; `(name, type, default)` optional |
159
+ | `optionals` | `list[tuple[str, type]]` | Optional output fields |
160
+
161
+ **Example:**
162
+
163
+ ```python
164
+ class Article(ModelSerializer):
165
+ title = models.CharField(max_length=200)
166
+ summary = models.TextField()
167
+ content = models.TextField()
168
+ author = models.ForeignKey(User, on_delete=models.CASCADE)
169
+ tags = models.ManyToManyField(Tag)
170
+ view_count = models.IntegerField(default=0)
171
+
172
+ class ReadSerializer:
173
+ # List view: minimal fields for performance
174
+ fields = ["id", "title", "summary", "author"]
175
+
176
+ class DetailSerializer:
177
+ # Detail view: all fields including expensive relations
178
+ fields = ["id", "title", "summary", "content", "author", "tags", "view_count"]
179
+ customs = [
180
+ ("reading_time", int, lambda obj: len(obj.content.split()) // 200),
181
+ ]
182
+ ```
183
+
184
+ **Generated Output (List):**
185
+
186
+ ```json
187
+ [
188
+ {"id": 1, "title": "Getting Started", "summary": "...", "author": {...}},
189
+ {"id": 2, "title": "Advanced Topics", "summary": "...", "author": {...}}
190
+ ]
191
+ ```
192
+
193
+ **Generated Output (Detail):**
194
+
195
+ ```json
196
+ {
197
+ "id": 1,
198
+ "title": "Getting Started",
199
+ "summary": "...",
200
+ "content": "Full article content here...",
201
+ "author": {...},
202
+ "tags": [{"id": 1, "name": "python"}, {"id": 2, "name": "django"}],
203
+ "view_count": 1234,
204
+ "reading_time": 5
205
+ }
206
+ ```
207
+
148
208
  ### UpdateSerializer
149
209
 
150
210
  Describes how to build an update (partial/full) input schema.
@@ -194,14 +254,15 @@ class User(ModelSerializer):
194
254
 
195
255
  ### Auto-Generated Schemas
196
256
 
197
- ModelSerializer automatically generates four schema types:
257
+ ModelSerializer automatically generates five schema types:
198
258
 
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 |
259
+ | Method | Schema Type | Purpose |
260
+ | ---------------------------- | ------------------ | ------------------------------------ |
261
+ | `generate_create_s()` | Input ("In") | POST endpoint payload |
262
+ | `generate_update_s()` | Input ("Patch") | PATCH/PUT endpoint payload |
263
+ | `generate_read_s(depth=1)` | Output ("Out") | List response with nested relations |
264
+ | `generate_detail_s(depth=1)` | Output ("Detail") | Single object response (retrieve) |
265
+ | `generate_related_s()` | Output ("Related") | Compact nested representation |
205
266
 
206
267
  **Example:**
207
268
 
@@ -219,6 +280,7 @@ class User(ModelSerializer):
219
280
  # Auto-generate schemas
220
281
  UserCreateSchema = User.generate_create_s()
221
282
  UserReadSchema = User.generate_read_s()
283
+ UserDetailSchema = User.generate_detail_s() # Returns None if DetailSerializer not defined
222
284
  UserUpdateSchema = User.generate_update_s()
223
285
  UserRelatedSchema = User.generate_related_s()
224
286
  ```
@@ -20,7 +20,7 @@ While both `ModelSerializer` and `Serializer` provide schema generation and CRUD
20
20
  | Schema generation | On-demand via generate_*() methods | On-demand via generate_*() methods |
21
21
  | Usage | Inherit from ModelSerializer | Separate serializer class |
22
22
  | Query optimization | QuerySet nested class | QuerySet nested class (inherited) |
23
- | Relation serializers | Auto-resolved | Explicit via relations_serializers (supports string refs) |
23
+ | Relation serializers | Auto-resolved | Explicit via relations_serializers (supports string refs & Union) |
24
24
 
25
25
  ## Key points
26
26
 
@@ -28,6 +28,7 @@ While both `ModelSerializer` and `Serializer` provide schema generation and CRUD
28
28
  - Generates read/create/update/related schemas on demand via ninja.orm.create_schema.
29
29
  - Supports explicit relation serializers for forward and reverse relations.
30
30
  - **Supports string references in `relations_serializers` for forward/circular dependencies**.
31
+ - **Supports Union types for polymorphic relations** (e.g., generic foreign keys, content types).
31
32
  - Plays nicely with APIViewSet to auto-wire schemas and queryset handling.
32
33
 
33
34
  ## Configuration
@@ -36,9 +37,10 @@ Define a Serializer subclass with a nested Meta:
36
37
 
37
38
  - **model**: Django model class
38
39
  - **schema_in**: SchemaModelConfig for create inputs
39
- - **schema_out**: SchemaModelConfig for read outputs
40
+ - **schema_out**: SchemaModelConfig for read outputs (list endpoint)
41
+ - **schema_detail**: SchemaModelConfig for detail outputs (retrieve endpoint)
40
42
  - **schema_update**: SchemaModelConfig for patch/update inputs
41
- - **relations_serializers**: Mapping of relation field name -> Serializer class **or string reference** (for forward/circular dependencies)
43
+ - **relations_serializers**: Mapping of relation field name -> Serializer class, **string reference**, or **Union of serializers** (supports forward/circular dependencies and polymorphic relations)
42
44
 
43
45
  SchemaModelConfig fields:
44
46
 
@@ -54,13 +56,37 @@ Generate schemas explicitly using these methods:
54
56
  ```python
55
57
  # Explicitly generate schemas when needed
56
58
  ArticleSerializer.generate_create_s() # Returns create (In) schema
57
- 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
58
61
  ArticleSerializer.generate_update_s() # Returns update (Patch) schema
59
62
  ArticleSerializer.generate_related_s() # Returns related (nested) schema
60
63
  ```
61
64
 
62
65
  Schemas support **forward references and circular dependencies** via string references in `relations_serializers`.
63
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
+
64
90
  ## Example: simple FK
65
91
 
66
92
  ```python
@@ -180,16 +206,191 @@ class ArticleSerializer(serializers.Serializer):
180
206
  }
181
207
  ```
182
208
 
209
+ **String Reference Formats:**
210
+
211
+ 1. **Class name in the same module:**
212
+ ```python
213
+ relations_serializers = {
214
+ "articles": "ArticleSerializer", # Resolved in current module
215
+ }
216
+ ```
217
+
218
+ 2. **Absolute import path:**
219
+ ```python
220
+ relations_serializers = {
221
+ "articles": "myapp.serializers.ArticleSerializer", # Full import path
222
+ }
223
+ ```
224
+
183
225
  **String Reference Requirements:**
184
- - String must be the class name of a serializer in the same module
226
+ - String can be the class name of a serializer in the same module, or an absolute import path
227
+ - Absolute paths use dot notation: `"package.module.ClassName"`
185
228
  - References are resolved lazily when schemas are generated
186
229
  - Both forward and circular references are supported
187
230
 
231
+ **Example: Cross-Module References with Absolute Paths**
232
+
233
+ ```python
234
+ # myapp/serializers.py
235
+ from ninja_aio.models import serializers
236
+ from . import models
237
+
238
+ class ArticleSerializer(serializers.Serializer):
239
+ class Meta:
240
+ model = models.Article
241
+ schema_out = serializers.SchemaModelConfig(
242
+ fields=["id", "title", "author"]
243
+ )
244
+ relations_serializers = {
245
+ # Reference a serializer from another module
246
+ "author": "users.serializers.UserSerializer",
247
+ }
248
+
249
+ # users/serializers.py
250
+ from ninja_aio.models import serializers
251
+ from . import models
252
+
253
+ class UserSerializer(serializers.Serializer):
254
+ class Meta:
255
+ model = models.User
256
+ schema_out = serializers.SchemaModelConfig(
257
+ fields=["id", "username", "email", "articles"]
258
+ )
259
+ relations_serializers = {
260
+ # Reference back to the article serializer
261
+ "articles": "myapp.serializers.ArticleSerializer",
262
+ }
263
+ ```
264
+
265
+ ## Union Types for Polymorphic Relations
266
+
267
+ You can use `Union` types in `relations_serializers` to handle polymorphic relationships where a field can reference multiple possible serializer types. This is particularly useful for generic foreign keys, content types, or any scenario where a relation can point to different model types.
268
+
269
+ ```python
270
+ from typing import Union
271
+ from ninja_aio.models import serializers
272
+ from . import models
273
+
274
+ class VideoSerializer(serializers.Serializer):
275
+ class Meta:
276
+ model = models.Video
277
+ schema_out = serializers.SchemaModelConfig(
278
+ fields=["id", "title", "duration", "url"]
279
+ )
280
+
281
+ class ImageSerializer(serializers.Serializer):
282
+ class Meta:
283
+ model = models.Image
284
+ schema_out = serializers.SchemaModelConfig(
285
+ fields=["id", "title", "width", "height", "url"]
286
+ )
287
+
288
+ class CommentSerializer(serializers.Serializer):
289
+ class Meta:
290
+ model = models.Comment
291
+ schema_out = serializers.SchemaModelConfig(
292
+ fields=["id", "text", "content_object"]
293
+ )
294
+ relations_serializers = {
295
+ # content_object can be a Video or Image
296
+ "content_object": Union[VideoSerializer, ImageSerializer],
297
+ }
298
+ ```
299
+
300
+ **Union Type Formats:**
301
+
302
+ 1. **Direct class references:**
303
+ ```python
304
+ relations_serializers = {
305
+ "field": Union[SerializerA, SerializerB],
306
+ }
307
+ ```
308
+
309
+ 2. **String references:**
310
+ ```python
311
+ relations_serializers = {
312
+ "field": Union["SerializerA", "SerializerB"],
313
+ }
314
+ ```
315
+
316
+ 3. **Mixed class and string references:**
317
+ ```python
318
+ relations_serializers = {
319
+ "field": Union[SerializerA, "SerializerB"],
320
+ }
321
+ ```
322
+
323
+ 4. **Absolute import paths:**
324
+ ```python
325
+ relations_serializers = {
326
+ "field": Union["myapp.serializers.SerializerA", SerializerB],
327
+ }
328
+ ```
329
+
330
+ **Use Cases for Union Types:**
331
+
332
+ - **Polymorphic relations:** Generic foreign keys or Django ContentType relations
333
+ - **Flexible APIs:** Different response formats for the same field based on runtime type
334
+ - **Gradual migrations:** Transitioning between different serializer implementations
335
+ - **Multi-tenant systems:** Different serialization requirements per tenant
336
+
337
+ **Complete Polymorphic Example:**
338
+
339
+ ```python
340
+ from typing import Union
341
+ from django.contrib.contenttypes.fields import GenericForeignKey
342
+ from ninja_aio.models import serializers
343
+
344
+ # Models
345
+ class Comment(models.Model):
346
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
347
+ object_id = models.PositiveIntegerField()
348
+ content_object = GenericForeignKey('content_type', 'object_id')
349
+ text = models.TextField()
350
+
351
+ # Serializers for different content types
352
+ class BlogPostSerializer(serializers.Serializer):
353
+ class Meta:
354
+ model = models.BlogPost
355
+ schema_out = serializers.SchemaModelConfig(
356
+ fields=["id", "title", "body", "published_at"]
357
+ )
358
+
359
+ class ProductSerializer(serializers.Serializer):
360
+ class Meta:
361
+ model = models.Product
362
+ schema_out = serializers.SchemaModelConfig(
363
+ fields=["id", "name", "price", "stock"]
364
+ )
365
+
366
+ class EventSerializer(serializers.Serializer):
367
+ class Meta:
368
+ model = models.Event
369
+ schema_out = serializers.SchemaModelConfig(
370
+ fields=["id", "name", "date", "location"]
371
+ )
372
+
373
+ # Comment serializer with Union support
374
+ class CommentSerializer(serializers.Serializer):
375
+ class Meta:
376
+ model = Comment
377
+ schema_out = serializers.SchemaModelConfig(
378
+ fields=["id", "text", "created_at", "content_object"]
379
+ )
380
+ relations_serializers = {
381
+ # Comments can be on blog posts, products, or events
382
+ "content_object": Union[BlogPostSerializer, ProductSerializer, EventSerializer],
383
+ }
384
+ ```
385
+
188
386
  Notes:
189
387
 
190
388
  - Forward relations are included as plain fields unless a related ModelSerializer/Serializer is declared.
191
389
  - Reverse relations require an entry in relations_serializers when using vanilla Django models.
192
390
  - When the related model is a ModelSerializer, related schemas can be auto-resolved.
391
+ - Absolute import paths are useful for cross-module references and avoiding circular import issues at module load time.
392
+ - Union types are resolved lazily, so forward and circular references work seamlessly.
393
+ - The schema generator will create a union of all possible schemas from the serializers in the Union.
193
394
 
194
395
  ## Using with APIViewSet
195
396
 
@@ -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.6.1"
3
+ __version__ = "2.8.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