django-ninja-aio-crud 2.6.1__tar.gz → 2.7.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 (103) hide show
  1. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/workflows/docs.yml +2 -0
  2. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/models/serializers.md +179 -3
  4. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/__init__.py +1 -1
  5. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/models/serializers.py +146 -27
  6. django_ninja_aio_crud-2.7.0/tests/test_serializers.py +291 -0
  7. django_ninja_aio_crud-2.6.1/tests/test_serializers.py +0 -106
  8. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/dependabot.yml +0 -0
  9. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/workflows/coverage.yml +0 -0
  10. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/workflows/publish.yml +0 -0
  11. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.gitignore +0 -0
  12. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.pre-commit-config.yaml +0 -0
  13. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/LICENSE +0 -0
  14. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/README.md +0 -0
  15. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/CNAME +0 -0
  16. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/authentication.md +0 -0
  17. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/models/model_serializer.md +0 -0
  18. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/models/model_util.md +0 -0
  19. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/pagination.md +0 -0
  20. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/renderers/orjson_renderer.md +0 -0
  21. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/api_view.md +0 -0
  22. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/api_view_set.md +0 -0
  23. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/decorators.md +0 -0
  24. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/mixins.md +0 -0
  25. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/auth.md +0 -0
  26. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/contributing.md +0 -0
  27. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/extra.css +0 -0
  28. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  29. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  30. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  31. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  32. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  33. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  34. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/installation.md +0 -0
  35. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/quick_start.md +0 -0
  36. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/bar-swagger.png +0 -0
  37. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/favicon.ico +0 -0
  38. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/foo-swagger.png +0 -0
  39. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/logo.png +0 -0
  40. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  41. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/index.md +0 -0
  42. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/release_notes.md +0 -0
  43. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/requirements.txt +0 -0
  44. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/authentication.md +0 -0
  45. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/crud.md +0 -0
  46. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/filtering.md +0 -0
  47. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/model.md +0 -0
  48. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/main.py +0 -0
  49. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/mkdocs.yml +0 -0
  50. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/api.py +0 -0
  51. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/auth.py +0 -0
  52. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/__init__.py +0 -0
  53. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/operations.py +0 -0
  54. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/views.py +0 -0
  55. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/exceptions.py +0 -0
  56. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/factory/__init__.py +0 -0
  57. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/factory/operations.py +0 -0
  58. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/helpers/__init__.py +0 -0
  59. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/helpers/api.py +0 -0
  60. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/helpers/query.py +0 -0
  61. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/models/__init__.py +0 -0
  62. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/models/utils.py +0 -0
  63. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/parsers.py +0 -0
  64. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/renders.py +0 -0
  65. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/__init__.py +0 -0
  66. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/api.py +0 -0
  67. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/generics.py +0 -0
  68. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/helpers.py +0 -0
  69. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/types.py +0 -0
  70. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/views/__init__.py +0 -0
  71. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/views/api.py +0 -0
  72. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/views/mixins.py +0 -0
  73. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/pyproject.toml +0 -0
  74. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/requirements.dev.txt +0 -0
  75. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/run-local-coverage.sh +0 -0
  76. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/__init__.py +0 -0
  77. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/__init__.py +0 -0
  78. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_decorators.py +0 -0
  79. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_exceptions_api.py +0 -0
  80. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_renderer_parser.py +0 -0
  81. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/__init__.py +0 -0
  82. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/literals.py +0 -0
  83. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/models.py +0 -0
  84. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/request.py +0 -0
  85. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/views.py +0 -0
  86. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/helpers/__init__.py +0 -0
  87. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/helpers/test_many_to_many_api.py +0 -0
  88. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/models/__init__.py +0 -0
  89. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/models/test_model_util.py +0 -0
  90. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/models/test_models_extra.py +0 -0
  91. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/__init__.py +0 -0
  92. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/models.py +0 -0
  93. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/schema.py +0 -0
  94. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/serializers.py +0 -0
  95. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/views.py +0 -0
  96. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_auth.py +0 -0
  97. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_decorators.py +0 -0
  98. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_exceptions.py +0 -0
  99. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_query_util.py +0 -0
  100. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_settings.py +0 -0
  101. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/views/__init__.py +0 -0
  102. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/views/test_views.py +0 -0
  103. {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/views/test_viewset.py +0 -0
@@ -18,6 +18,7 @@ on:
18
18
  - "2.4"
19
19
  - "2.5"
20
20
  - "2.6"
21
+ - "2.6.1"
21
22
  make_latest:
22
23
  description: 'Set as "latest" and default?'
23
24
  type: boolean
@@ -39,6 +40,7 @@ on:
39
40
  - "2.4"
40
41
  - "2.5"
41
42
  - "2.6"
43
+ - "2.6.1"
42
44
  delete_confirm:
43
45
  description: 'Confirm deletion of the selected version'
44
46
  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.7.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -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
@@ -38,7 +39,7 @@ Define a Serializer subclass with a nested Meta:
38
39
  - **schema_in**: SchemaModelConfig for create inputs
39
40
  - **schema_out**: SchemaModelConfig for read outputs
40
41
  - **schema_update**: SchemaModelConfig for patch/update inputs
41
- - **relations_serializers**: Mapping of relation field name -> Serializer class **or string reference** (for forward/circular dependencies)
42
+ - **relations_serializers**: Mapping of relation field name -> Serializer class, **string reference**, or **Union of serializers** (supports forward/circular dependencies and polymorphic relations)
42
43
 
43
44
  SchemaModelConfig fields:
44
45
 
@@ -180,16 +181,191 @@ class ArticleSerializer(serializers.Serializer):
180
181
  }
181
182
  ```
182
183
 
184
+ **String Reference Formats:**
185
+
186
+ 1. **Class name in the same module:**
187
+ ```python
188
+ relations_serializers = {
189
+ "articles": "ArticleSerializer", # Resolved in current module
190
+ }
191
+ ```
192
+
193
+ 2. **Absolute import path:**
194
+ ```python
195
+ relations_serializers = {
196
+ "articles": "myapp.serializers.ArticleSerializer", # Full import path
197
+ }
198
+ ```
199
+
183
200
  **String Reference Requirements:**
184
- - String must be the class name of a serializer in the same module
201
+ - String can be the class name of a serializer in the same module, or an absolute import path
202
+ - Absolute paths use dot notation: `"package.module.ClassName"`
185
203
  - References are resolved lazily when schemas are generated
186
204
  - Both forward and circular references are supported
187
205
 
206
+ **Example: Cross-Module References with Absolute Paths**
207
+
208
+ ```python
209
+ # myapp/serializers.py
210
+ from ninja_aio.models import serializers
211
+ from . import models
212
+
213
+ class ArticleSerializer(serializers.Serializer):
214
+ class Meta:
215
+ model = models.Article
216
+ schema_out = serializers.SchemaModelConfig(
217
+ fields=["id", "title", "author"]
218
+ )
219
+ relations_serializers = {
220
+ # Reference a serializer from another module
221
+ "author": "users.serializers.UserSerializer",
222
+ }
223
+
224
+ # users/serializers.py
225
+ from ninja_aio.models import serializers
226
+ from . import models
227
+
228
+ class UserSerializer(serializers.Serializer):
229
+ class Meta:
230
+ model = models.User
231
+ schema_out = serializers.SchemaModelConfig(
232
+ fields=["id", "username", "email", "articles"]
233
+ )
234
+ relations_serializers = {
235
+ # Reference back to the article serializer
236
+ "articles": "myapp.serializers.ArticleSerializer",
237
+ }
238
+ ```
239
+
240
+ ## Union Types for Polymorphic Relations
241
+
242
+ 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.
243
+
244
+ ```python
245
+ from typing import Union
246
+ from ninja_aio.models import serializers
247
+ from . import models
248
+
249
+ class VideoSerializer(serializers.Serializer):
250
+ class Meta:
251
+ model = models.Video
252
+ schema_out = serializers.SchemaModelConfig(
253
+ fields=["id", "title", "duration", "url"]
254
+ )
255
+
256
+ class ImageSerializer(serializers.Serializer):
257
+ class Meta:
258
+ model = models.Image
259
+ schema_out = serializers.SchemaModelConfig(
260
+ fields=["id", "title", "width", "height", "url"]
261
+ )
262
+
263
+ class CommentSerializer(serializers.Serializer):
264
+ class Meta:
265
+ model = models.Comment
266
+ schema_out = serializers.SchemaModelConfig(
267
+ fields=["id", "text", "content_object"]
268
+ )
269
+ relations_serializers = {
270
+ # content_object can be a Video or Image
271
+ "content_object": Union[VideoSerializer, ImageSerializer],
272
+ }
273
+ ```
274
+
275
+ **Union Type Formats:**
276
+
277
+ 1. **Direct class references:**
278
+ ```python
279
+ relations_serializers = {
280
+ "field": Union[SerializerA, SerializerB],
281
+ }
282
+ ```
283
+
284
+ 2. **String references:**
285
+ ```python
286
+ relations_serializers = {
287
+ "field": Union["SerializerA", "SerializerB"],
288
+ }
289
+ ```
290
+
291
+ 3. **Mixed class and string references:**
292
+ ```python
293
+ relations_serializers = {
294
+ "field": Union[SerializerA, "SerializerB"],
295
+ }
296
+ ```
297
+
298
+ 4. **Absolute import paths:**
299
+ ```python
300
+ relations_serializers = {
301
+ "field": Union["myapp.serializers.SerializerA", SerializerB],
302
+ }
303
+ ```
304
+
305
+ **Use Cases for Union Types:**
306
+
307
+ - **Polymorphic relations:** Generic foreign keys or Django ContentType relations
308
+ - **Flexible APIs:** Different response formats for the same field based on runtime type
309
+ - **Gradual migrations:** Transitioning between different serializer implementations
310
+ - **Multi-tenant systems:** Different serialization requirements per tenant
311
+
312
+ **Complete Polymorphic Example:**
313
+
314
+ ```python
315
+ from typing import Union
316
+ from django.contrib.contenttypes.fields import GenericForeignKey
317
+ from ninja_aio.models import serializers
318
+
319
+ # Models
320
+ class Comment(models.Model):
321
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
322
+ object_id = models.PositiveIntegerField()
323
+ content_object = GenericForeignKey('content_type', 'object_id')
324
+ text = models.TextField()
325
+
326
+ # Serializers for different content types
327
+ class BlogPostSerializer(serializers.Serializer):
328
+ class Meta:
329
+ model = models.BlogPost
330
+ schema_out = serializers.SchemaModelConfig(
331
+ fields=["id", "title", "body", "published_at"]
332
+ )
333
+
334
+ class ProductSerializer(serializers.Serializer):
335
+ class Meta:
336
+ model = models.Product
337
+ schema_out = serializers.SchemaModelConfig(
338
+ fields=["id", "name", "price", "stock"]
339
+ )
340
+
341
+ class EventSerializer(serializers.Serializer):
342
+ class Meta:
343
+ model = models.Event
344
+ schema_out = serializers.SchemaModelConfig(
345
+ fields=["id", "name", "date", "location"]
346
+ )
347
+
348
+ # Comment serializer with Union support
349
+ class CommentSerializer(serializers.Serializer):
350
+ class Meta:
351
+ model = Comment
352
+ schema_out = serializers.SchemaModelConfig(
353
+ fields=["id", "text", "created_at", "content_object"]
354
+ )
355
+ relations_serializers = {
356
+ # Comments can be on blog posts, products, or events
357
+ "content_object": Union[BlogPostSerializer, ProductSerializer, EventSerializer],
358
+ }
359
+ ```
360
+
188
361
  Notes:
189
362
 
190
363
  - Forward relations are included as plain fields unless a related ModelSerializer/Serializer is declared.
191
364
  - Reverse relations require an entry in relations_serializers when using vanilla Django models.
192
365
  - When the related model is a ModelSerializer, related schemas can be auto-resolved.
366
+ - Absolute import paths are useful for cross-module references and avoiding circular import issues at module load time.
367
+ - Union types are resolved lazily, so forward and circular references work seamlessly.
368
+ - The schema generator will create a union of all possible schemas from the serializers in the Union.
193
369
 
194
370
  ## Using with APIViewSet
195
371
 
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.6.1"
3
+ __version__ = "2.7.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,4 +1,4 @@
1
- from typing import Any, List, Optional
1
+ from typing import Any, List, Optional, Union, get_args, get_origin, ForwardRef
2
2
  import warnings
3
3
  import sys
4
4
 
@@ -69,19 +69,14 @@ class BaseSerializer:
69
69
  raise NotImplementedError
70
70
 
71
71
  @classmethod
72
- def _resolve_serializer_reference(cls, serializer_ref: str | type) -> type:
72
+ def _resolve_string_reference(cls, string_ref: str) -> type:
73
73
  """
74
- Resolve a serializer reference that may be a string or a class.
75
-
76
- This method performs lazy resolution, meaning it will attempt to resolve
77
- string references only when called, allowing for forward references and
78
- circular dependencies between serializers in the same module.
74
+ Resolve a string serializer reference to an actual class.
79
75
 
80
76
  Parameters
81
77
  ----------
82
- serializer_ref : str | type
83
- Either a string reference to a serializer class name in the same module,
84
- or an actual serializer class.
78
+ string_ref : str
79
+ String reference (local class name or absolute import path).
85
80
 
86
81
  Returns
87
82
  -------
@@ -93,35 +88,152 @@ class BaseSerializer:
93
88
  ValueError
94
89
  If the string reference cannot be resolved.
95
90
  """
96
- # If it's already a class, return it directly
97
- if not isinstance(serializer_ref, str):
98
- return serializer_ref
91
+ # Check if it's an absolute import path (contains dots)
92
+ if "." in string_ref:
93
+ # Absolute import path: "myapp.serializers.UserSerializer"
94
+ module_path, class_name = string_ref.rsplit(".", 1)
99
95
 
100
- # Get the module where the current serializer class is defined
96
+ try:
97
+ # Try to get or import the module
98
+ module = sys.modules.get(module_path)
99
+ if module is None:
100
+ import importlib
101
+ module = importlib.import_module(module_path)
102
+
103
+ # Get the serializer class from the module
104
+ serializer_class = getattr(module, class_name, None)
105
+
106
+ if serializer_class is None:
107
+ raise ValueError(
108
+ f"Cannot resolve serializer reference '{string_ref}': "
109
+ f"class '{class_name}' not found in module '{module_path}'."
110
+ )
111
+
112
+ return serializer_class
113
+ except ImportError as e:
114
+ raise ValueError(
115
+ f"Cannot resolve serializer reference '{string_ref}': "
116
+ f"failed to import module '{module_path}': {e}"
117
+ )
118
+
119
+ # Local reference: simple class name in the same module
101
120
  module = sys.modules.get(cls.__module__)
102
121
 
103
122
  if module is None:
104
123
  raise ValueError(
105
- f"Cannot resolve serializer reference '{serializer_ref}': "
124
+ f"Cannot resolve serializer reference '{string_ref}': "
106
125
  f"module '{cls.__module__}' not found in sys.modules."
107
126
  )
108
127
 
109
- # Try to get the serializer class from the module
110
- serializer_class = getattr(module, serializer_ref, None)
128
+ serializer_class = getattr(module, string_ref, None)
111
129
 
112
130
  if serializer_class is None:
113
131
  raise ValueError(
114
- f"Cannot resolve serializer reference '{serializer_ref}' in module '{cls.__module__}'. "
115
- f"Make sure the serializer class '{serializer_ref}' is defined in the same module as {cls.__name__}."
132
+ f"Cannot resolve serializer reference '{string_ref}' in module '{cls.__module__}'. "
133
+ f"Make sure the serializer class '{string_ref}' is defined in the same module as {cls.__name__}."
116
134
  )
117
135
 
118
136
  return serializer_class
119
137
 
138
+ @classmethod
139
+ def _resolve_serializer_reference(cls, serializer_ref: str | type | Any) -> type | Any:
140
+ """
141
+ Resolve a serializer reference that may be a string, a class, or a Union of serializers.
142
+
143
+ This method performs lazy resolution, meaning it will attempt to resolve
144
+ string references only when called, allowing for forward references and
145
+ circular dependencies between serializers in the same module.
146
+
147
+ Parameters
148
+ ----------
149
+ serializer_ref : str | type | Union
150
+ Either a string reference to a serializer class, an actual serializer class,
151
+ or a Union of serializer references. String references can be:
152
+ - A class name in the same module (e.g., "UserSerializer")
153
+ - An absolute import path (e.g., "myapp.serializers.UserSerializer")
154
+
155
+ Returns
156
+ -------
157
+ type | Union
158
+ The resolved serializer class or Union of serializer classes.
159
+
160
+ Raises
161
+ ------
162
+ ValueError
163
+ If the string reference cannot be resolved.
164
+
165
+ Examples
166
+ --------
167
+ >>> # Single reference
168
+ >>> cls._resolve_serializer_reference("UserSerializer")
169
+ >>> cls._resolve_serializer_reference(UserSerializer)
170
+ >>>
171
+ >>> # Union reference
172
+ >>> from typing import Union
173
+ >>> cls._resolve_serializer_reference(Union[UserSerializer, AdminSerializer])
174
+ >>> cls._resolve_serializer_reference(Union["UserSerializer", "AdminSerializer"])
175
+ """
176
+ # Handle Union types
177
+ origin = get_origin(serializer_ref)
178
+ if origin is Union:
179
+ resolved_types = tuple(
180
+ cls._resolve_serializer_reference(arg)
181
+ for arg in get_args(serializer_ref)
182
+ )
183
+ # Optimize single-type unions
184
+ if len(resolved_types) == 1:
185
+ return resolved_types[0]
186
+ # Create Union using indexing syntax for Python 3.10+ compatibility
187
+ return Union[resolved_types]
188
+
189
+ # Handle ForwardRef (created when using Union["StringType"])
190
+ if isinstance(serializer_ref, ForwardRef):
191
+ return cls._resolve_serializer_reference(serializer_ref.__forward_arg__)
192
+
193
+ # Handle string references
194
+ if isinstance(serializer_ref, str):
195
+ return cls._resolve_string_reference(serializer_ref)
196
+
197
+ # Already a class, return as-is
198
+ return serializer_ref
199
+
120
200
  @classmethod
121
201
  def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
122
202
  # Optional in subclasses. Default to no explicit relation serializers.
123
203
  return {}
124
204
 
205
+ @classmethod
206
+ def _generate_union_schema(cls, resolved_union: Any) -> Any:
207
+ """
208
+ Generate a Union schema from multiple resolved serializers.
209
+
210
+ Parameters
211
+ ----------
212
+ resolved_union : Union
213
+ A Union type containing resolved serializer classes.
214
+
215
+ Returns
216
+ -------
217
+ Schema | Union[Schema, ...] | None
218
+ Union of generated schemas or None if all schemas are None.
219
+ """
220
+ # Generate schemas for each serializer in the Union
221
+ schemas = tuple(
222
+ serializer_type.generate_related_s()
223
+ for serializer_type in get_args(resolved_union)
224
+ if serializer_type.generate_related_s() is not None
225
+ )
226
+
227
+ if not schemas:
228
+ return None
229
+
230
+ # Optimize single-schema unions
231
+ if len(schemas) == 1:
232
+ return schemas[0]
233
+
234
+ # Create Union of schemas using indexing syntax for Python 3.10+ compatibility
235
+ return Union[schemas]
236
+
125
237
  @classmethod
126
238
  def _resolve_relation_schema(cls, field_name: str, rel_model: models.Model):
127
239
  """
@@ -136,23 +248,30 @@ class BaseSerializer:
136
248
 
137
249
  Returns
138
250
  -------
139
- Schema | None
140
- Generated schema or None if cannot be resolved.
251
+ Schema | Union[Schema, ...] | None
252
+ Generated schema, Union of schemas, or None if cannot be resolved.
141
253
  """
142
- # Check if related model is a ModelSerializer with readable fields
254
+ # Auto-resolve ModelSerializer with readable fields
143
255
  if isinstance(rel_model, ModelSerializerMeta):
144
256
  if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
145
257
  return rel_model.generate_related_s()
146
258
  return None
147
259
 
148
- # Fall back to explicit serializer mapping
260
+ # Resolve from explicit serializer mapping
149
261
  rel_serializers = cls._get_relations_serializers() or {}
150
262
  serializer_ref = rel_serializers.get(field_name)
151
- if serializer_ref:
152
- serializer = cls._resolve_serializer_reference(serializer_ref)
153
- return serializer.generate_related_s()
154
263
 
155
- return None
264
+ if not serializer_ref:
265
+ return None
266
+
267
+ resolved = cls._resolve_serializer_reference(serializer_ref)
268
+
269
+ # Handle Union of serializers
270
+ if get_origin(resolved) is Union:
271
+ return cls._generate_union_schema(resolved)
272
+
273
+ # Handle single serializer
274
+ return resolved.generate_related_s()
156
275
 
157
276
  @classmethod
158
277
  def _is_special_field(
@@ -0,0 +1,291 @@
1
+ import warnings
2
+ from typing import Union
3
+ from django.test import TestCase, tag
4
+
5
+ from tests.test_app.models import TestModelForeignKey, TestModelReverseForeignKey
6
+ from ninja_aio.models import serializers
7
+
8
+
9
+ class TestModelForeignKeySerializer(serializers.Serializer):
10
+ class Meta:
11
+ model = TestModelForeignKey
12
+ schema_in = serializers.SchemaModelConfig(
13
+ fields=["name", "description", "test_model"]
14
+ )
15
+ schema_out = serializers.SchemaModelConfig(
16
+ fields=["id", "name", "description", "test_model"]
17
+ )
18
+
19
+
20
+ class TestModelReverseForeignKeySerializer(serializers.Serializer):
21
+ class Meta:
22
+ model = TestModelReverseForeignKey
23
+ schema_in = serializers.SchemaModelConfig(fields=["name", "description"])
24
+ schema_out = serializers.SchemaModelConfig(
25
+ fields=["id", "name", "description", "test_model_foreign_keys"]
26
+ )
27
+ relations_serializers = {
28
+ "test_model_foreign_keys": TestModelForeignKeySerializer,
29
+ }
30
+
31
+
32
+ # Test serializers for Union tests - defined at module level so they can be resolved
33
+ class AltSerializer(serializers.Serializer):
34
+ class Meta:
35
+ model = TestModelForeignKey
36
+ schema_out = serializers.SchemaModelConfig(
37
+ fields=["id", "name"]
38
+ )
39
+
40
+
41
+ class AltStringSerializer(serializers.Serializer):
42
+ class Meta:
43
+ model = TestModelForeignKey
44
+ schema_out = serializers.SchemaModelConfig(
45
+ fields=["id", "description"]
46
+ )
47
+
48
+
49
+ class MixedAltSerializer(serializers.Serializer):
50
+ class Meta:
51
+ model = TestModelForeignKey
52
+ schema_out = serializers.SchemaModelConfig(
53
+ fields=["id", "name", "description"]
54
+ )
55
+
56
+
57
+ class LocalTestSerializer(serializers.Serializer):
58
+ class Meta:
59
+ model = TestModelForeignKey
60
+ schema_out = serializers.SchemaModelConfig(fields=["id"])
61
+
62
+
63
+ @tag("serializers")
64
+ class SerializersTestCase(TestCase):
65
+
66
+ @classmethod
67
+ def setUpTestData(cls):
68
+ cls.serializer_fk = TestModelForeignKeySerializer
69
+ cls.serializer_rfk = TestModelReverseForeignKeySerializer
70
+ warnings.simplefilter("ignore", UserWarning) # Ignora tutti i UserWarning
71
+
72
+ def test_generate_schema_out(self):
73
+ schema_out_fk = self.serializer_fk.generate_read_s()
74
+ for f in ["id", "name", "description", "test_model"]:
75
+ self.assertIn(f, schema_out_fk.model_fields)
76
+
77
+ schema_out_rfk = self.serializer_rfk.generate_read_s()
78
+ for f in ["id", "name", "description", "test_model_foreign_keys"]:
79
+ self.assertIn(f, schema_out_rfk.model_fields)
80
+
81
+ def test_generate_schema_in(self):
82
+ schema_in_fk = self.serializer_fk.generate_create_s()
83
+ # In schema should include declared input fields
84
+ for f in ["name", "description", "test_model"]:
85
+ self.assertIn(f, schema_in_fk.model_fields)
86
+ self.assertNotIn("id", schema_in_fk.model_fields)
87
+
88
+ schema_in_rfk = self.serializer_rfk.generate_create_s()
89
+ for f in ["name", "description"]:
90
+ self.assertIn(f, schema_in_rfk.model_fields)
91
+
92
+ def test_generate_schema_update(self):
93
+ # If no fields provided for update, optional fields should be honored when declared
94
+ # Here no explicit update config exists, so update schema may be None or empty depending on implementation
95
+ schema_patch_fk = self.serializer_fk.generate_update_s()
96
+ # Implementation returns a schema when fields/customs/excludes exist; otherwise may fallback to optionals.
97
+ # Our Meta doesn't define update, so ensure function doesn't crash and returns a Schema or None
98
+ self.assertTrue(
99
+ schema_patch_fk is None or hasattr(schema_patch_fk, "model_fields")
100
+ )
101
+
102
+ schema_patch_rfk = self.serializer_rfk.generate_update_s()
103
+ self.assertTrue(
104
+ schema_patch_rfk is None or hasattr(schema_patch_rfk, "model_fields")
105
+ )
106
+
107
+ def test_generate_related_schema(self):
108
+ related_fk = self.serializer_fk.generate_related_s()
109
+ # Related schema should include non-relational read fields only
110
+ for f in ["id", "name", "description"]:
111
+ self.assertIn(f, related_fk.model_fields)
112
+ # Forward relation declared on fk read fields should still be present as plain field in read, but not in related
113
+ self.assertNotIn("test_model", related_fk.model_fields)
114
+
115
+ related_rfk = self.serializer_rfk.generate_related_s()
116
+ for f in ["id", "name", "description"]:
117
+ self.assertIn(f, related_rfk.model_fields)
118
+ # Reverse relation should be excluded from related schema
119
+ self.assertNotIn("test_model_foreign_keys", related_rfk.model_fields)
120
+
121
+ def test_relation_serializer_required_when_mapping_provided(self):
122
+ # When relations_serializers mapping exists, any relation field listed in read fields must have a mapping
123
+ class BadSerializer(serializers.Serializer):
124
+ class Meta:
125
+ model = TestModelReverseForeignKey
126
+ schema_out = serializers.SchemaModelConfig(
127
+ fields=["id", "name", "description", "test_model_foreign_keys"]
128
+ )
129
+ relations_serializers = {}
130
+
131
+ def test_relation_serializer_inclusion(self):
132
+ # Ensure that providing relations_serializers yields nested related schema in read
133
+ schema_out_rfk = self.serializer_rfk.generate_read_s()
134
+ # The reverse relation should be represented as a field; the nested schema type comes from ninja's create_schema.
135
+ self.assertIn("test_model_foreign_keys", schema_out_rfk.model_fields)
136
+ # Also ensure base fields present
137
+ for f in ["id", "name", "description"]:
138
+ self.assertIn(f, schema_out_rfk.model_fields)
139
+
140
+
141
+ @tag("serializers", "union")
142
+ class UnionSerializerTestCase(TestCase):
143
+ """Test cases for Union serializer references support."""
144
+
145
+ def setUp(self):
146
+ warnings.simplefilter("ignore", UserWarning)
147
+
148
+ def test_union_with_direct_class_references(self):
149
+ """Test Union with direct class references."""
150
+
151
+ class UnionTestSerializer(serializers.Serializer):
152
+ class Meta:
153
+ model = TestModelReverseForeignKey
154
+ schema_out = serializers.SchemaModelConfig(
155
+ fields=["id", "name", "test_model_foreign_keys"]
156
+ )
157
+ relations_serializers = {
158
+ "test_model_foreign_keys": Union[TestModelForeignKeySerializer, AltSerializer],
159
+ }
160
+
161
+ # Should resolve without errors
162
+ schema = UnionTestSerializer.generate_read_s()
163
+ self.assertIsNotNone(schema)
164
+ self.assertIn("test_model_foreign_keys", schema.model_fields)
165
+
166
+ def test_union_with_string_references(self):
167
+ """Test Union with string references."""
168
+
169
+ class UnionStringTestSerializer(serializers.Serializer):
170
+ class Meta:
171
+ model = TestModelReverseForeignKey
172
+ schema_out = serializers.SchemaModelConfig(
173
+ fields=["id", "name", "test_model_foreign_keys"]
174
+ )
175
+ relations_serializers = {
176
+ "test_model_foreign_keys": Union["TestModelForeignKeySerializer", "AltStringSerializer"],
177
+ }
178
+
179
+ # Should resolve without errors
180
+ schema = UnionStringTestSerializer.generate_read_s()
181
+ self.assertIsNotNone(schema)
182
+ self.assertIn("test_model_foreign_keys", schema.model_fields)
183
+
184
+ def test_union_with_mixed_references(self):
185
+ """Test Union with mixed class and string references."""
186
+
187
+ class UnionMixedTestSerializer(serializers.Serializer):
188
+ class Meta:
189
+ model = TestModelReverseForeignKey
190
+ schema_out = serializers.SchemaModelConfig(
191
+ fields=["id", "name", "test_model_foreign_keys"]
192
+ )
193
+ relations_serializers = {
194
+ "test_model_foreign_keys": Union[MixedAltSerializer, "TestModelForeignKeySerializer"],
195
+ }
196
+
197
+ # Should resolve without errors
198
+ schema = UnionMixedTestSerializer.generate_read_s()
199
+ self.assertIsNotNone(schema)
200
+ self.assertIn("test_model_foreign_keys", schema.model_fields)
201
+
202
+ def test_union_with_absolute_import_path(self):
203
+ """Test Union with absolute import path string references."""
204
+
205
+ class UnionAbsolutePathSerializer(serializers.Serializer):
206
+ class Meta:
207
+ model = TestModelReverseForeignKey
208
+ schema_out = serializers.SchemaModelConfig(
209
+ fields=["id", "name", "test_model_foreign_keys"]
210
+ )
211
+ relations_serializers = {
212
+ "test_model_foreign_keys": Union[
213
+ "tests.test_serializers.TestModelForeignKeySerializer",
214
+ TestModelForeignKeySerializer,
215
+ ],
216
+ }
217
+
218
+ # Should resolve without errors
219
+ schema = UnionAbsolutePathSerializer.generate_read_s()
220
+ self.assertIsNotNone(schema)
221
+ self.assertIn("test_model_foreign_keys", schema.model_fields)
222
+
223
+ def test_resolve_serializer_reference_with_union(self):
224
+ """Test _resolve_serializer_reference directly with Union types."""
225
+ from typing import get_args, get_origin
226
+
227
+ # Test with Union of DIFFERENT classes (Union of same type gets optimized away by Python)
228
+ union_ref = Union[TestModelForeignKeySerializer, AltSerializer]
229
+ resolved = TestModelForeignKeySerializer._resolve_serializer_reference(union_ref)
230
+
231
+ # Check that it returns a Union (using reduce with or_ creates a union-like structure)
232
+ # The resolved type should be a union of the two serializers
233
+ self.assertEqual(get_origin(resolved), Union)
234
+ resolved_args = get_args(resolved)
235
+ self.assertEqual(len(resolved_args), 2)
236
+ # Should contain both serializer classes
237
+ self.assertIn(TestModelForeignKeySerializer, resolved_args)
238
+ self.assertIn(AltSerializer, resolved_args)
239
+
240
+ def test_resolve_serializer_reference_with_string_union(self):
241
+ """Test _resolve_serializer_reference with Union of strings."""
242
+ from typing import get_args, get_origin
243
+
244
+ # Test with Union of string references
245
+ union_ref = Union["TestModelForeignKeySerializer", "LocalTestSerializer"]
246
+ resolved = LocalTestSerializer._resolve_serializer_reference(union_ref)
247
+
248
+ # Check that it returns a Union
249
+ self.assertEqual(get_origin(resolved), Union)
250
+ resolved_types = get_args(resolved)
251
+ self.assertEqual(len(resolved_types), 2)
252
+
253
+ # Verify that both serializers are resolved correctly
254
+ self.assertIn(TestModelForeignKeySerializer, resolved_types)
255
+ self.assertIn(LocalTestSerializer, resolved_types)
256
+
257
+ def test_single_serializer_still_works(self):
258
+ """Ensure single serializer references still work as before."""
259
+
260
+ class SingleRefSerializer(serializers.Serializer):
261
+ class Meta:
262
+ model = TestModelReverseForeignKey
263
+ schema_out = serializers.SchemaModelConfig(
264
+ fields=["id", "name", "test_model_foreign_keys"]
265
+ )
266
+ relations_serializers = {
267
+ "test_model_foreign_keys": TestModelForeignKeySerializer,
268
+ }
269
+
270
+ # Should work exactly as before
271
+ schema = SingleRefSerializer.generate_read_s()
272
+ self.assertIsNotNone(schema)
273
+ self.assertIn("test_model_foreign_keys", schema.model_fields)
274
+
275
+ def test_single_string_serializer_still_works(self):
276
+ """Ensure single string serializer references still work as before."""
277
+
278
+ class SingleStringRefSerializer(serializers.Serializer):
279
+ class Meta:
280
+ model = TestModelReverseForeignKey
281
+ schema_out = serializers.SchemaModelConfig(
282
+ fields=["id", "name", "test_model_foreign_keys"]
283
+ )
284
+ relations_serializers = {
285
+ "test_model_foreign_keys": "TestModelForeignKeySerializer",
286
+ }
287
+
288
+ # Should work exactly as before
289
+ schema = SingleStringRefSerializer.generate_read_s()
290
+ self.assertIsNotNone(schema)
291
+ self.assertIn("test_model_foreign_keys", schema.model_fields)
@@ -1,106 +0,0 @@
1
- import warnings
2
- from django.test import TestCase, tag
3
-
4
- from tests.test_app.models import TestModelForeignKey, TestModelReverseForeignKey
5
- from ninja_aio.models import serializers
6
-
7
-
8
- class TestModelForeignKeySerializer(serializers.Serializer):
9
- class Meta:
10
- model = TestModelForeignKey
11
- schema_in = serializers.SchemaModelConfig(
12
- fields=["name", "description", "test_model"]
13
- )
14
- schema_out = serializers.SchemaModelConfig(
15
- fields=["id", "name", "description", "test_model"]
16
- )
17
-
18
-
19
- class TestModelReverseForeignKeySerializer(serializers.Serializer):
20
- class Meta:
21
- model = TestModelReverseForeignKey
22
- schema_in = serializers.SchemaModelConfig(fields=["name", "description"])
23
- schema_out = serializers.SchemaModelConfig(
24
- fields=["id", "name", "description", "test_model_foreign_keys"]
25
- )
26
- relations_serializers = {
27
- "test_model_foreign_keys": TestModelForeignKeySerializer,
28
- }
29
-
30
-
31
- @tag("serializers")
32
- class SerializersTestCase(TestCase):
33
-
34
- @classmethod
35
- def setUpTestData(cls):
36
- cls.serializer_fk = TestModelForeignKeySerializer
37
- cls.serializer_rfk = TestModelReverseForeignKeySerializer
38
- warnings.simplefilter("ignore", UserWarning) # Ignora tutti i UserWarning
39
-
40
- def test_generate_schema_out(self):
41
- schema_out_fk = self.serializer_fk.generate_read_s()
42
- for f in ["id", "name", "description", "test_model"]:
43
- self.assertIn(f, schema_out_fk.model_fields)
44
-
45
- schema_out_rfk = self.serializer_rfk.generate_read_s()
46
- for f in ["id", "name", "description", "test_model_foreign_keys"]:
47
- self.assertIn(f, schema_out_rfk.model_fields)
48
-
49
- def test_generate_schema_in(self):
50
- schema_in_fk = self.serializer_fk.generate_create_s()
51
- # In schema should include declared input fields
52
- for f in ["name", "description", "test_model"]:
53
- self.assertIn(f, schema_in_fk.model_fields)
54
- self.assertNotIn("id", schema_in_fk.model_fields)
55
-
56
- schema_in_rfk = self.serializer_rfk.generate_create_s()
57
- for f in ["name", "description"]:
58
- self.assertIn(f, schema_in_rfk.model_fields)
59
-
60
- def test_generate_schema_update(self):
61
- # If no fields provided for update, optional fields should be honored when declared
62
- # Here no explicit update config exists, so update schema may be None or empty depending on implementation
63
- schema_patch_fk = self.serializer_fk.generate_update_s()
64
- # Implementation returns a schema when fields/customs/excludes exist; otherwise may fallback to optionals.
65
- # Our Meta doesn't define update, so ensure function doesn't crash and returns a Schema or None
66
- self.assertTrue(
67
- schema_patch_fk is None or hasattr(schema_patch_fk, "model_fields")
68
- )
69
-
70
- schema_patch_rfk = self.serializer_rfk.generate_update_s()
71
- self.assertTrue(
72
- schema_patch_rfk is None or hasattr(schema_patch_rfk, "model_fields")
73
- )
74
-
75
- def test_generate_related_schema(self):
76
- related_fk = self.serializer_fk.generate_related_s()
77
- # Related schema should include non-relational read fields only
78
- for f in ["id", "name", "description"]:
79
- self.assertIn(f, related_fk.model_fields)
80
- # Forward relation declared on fk read fields should still be present as plain field in read, but not in related
81
- self.assertNotIn("test_model", related_fk.model_fields)
82
-
83
- related_rfk = self.serializer_rfk.generate_related_s()
84
- for f in ["id", "name", "description"]:
85
- self.assertIn(f, related_rfk.model_fields)
86
- # Reverse relation should be excluded from related schema
87
- self.assertNotIn("test_model_foreign_keys", related_rfk.model_fields)
88
-
89
- def test_relation_serializer_required_when_mapping_provided(self):
90
- # When relations_serializers mapping exists, any relation field listed in read fields must have a mapping
91
- class BadSerializer(serializers.Serializer):
92
- class Meta:
93
- model = TestModelReverseForeignKey
94
- schema_out = serializers.SchemaModelConfig(
95
- fields=["id", "name", "description", "test_model_foreign_keys"]
96
- )
97
- relations_serializers = {}
98
-
99
- def test_relation_serializer_inclusion(self):
100
- # Ensure that providing relations_serializers yields nested related schema in read
101
- schema_out_rfk = self.serializer_rfk.generate_read_s()
102
- # The reverse relation should be represented as a field; the nested schema type comes from ninja's create_schema.
103
- self.assertIn("test_model_foreign_keys", schema_out_rfk.model_fields)
104
- # Also ensure base fields present
105
- for f in ["id", "name", "description"]:
106
- self.assertIn(f, schema_out_rfk.model_fields)