django-ninja-aio-crud 2.16.0__tar.gz → 2.16.2__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.16.0 → django_ninja_aio_crud-2.16.2}/.github/workflows/docs.yml +2 -0
  2. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/views/api_view_set.md +40 -0
  4. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/views/decorators.md +17 -0
  5. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/quick_start.md +9 -7
  6. django_ninja_aio_crud-2.16.2/docs/getting_started/quick_start_serializer.md +220 -0
  7. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/mkdocs.yml +2 -1
  8. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/__init__.py +1 -1
  9. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/exceptions.py +1 -1
  10. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/models/serializers.py +17 -8
  11. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_serializers.py +155 -0
  12. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/.github/dependabot.yml +0 -0
  13. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/.github/workflows/coverage.yml +0 -0
  14. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/.github/workflows/publish.yml +0 -0
  15. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/.gitignore +0 -0
  16. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/.pre-commit-config.yaml +0 -0
  17. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/LICENSE +0 -0
  18. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/README.md +0 -0
  19. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/CNAME +0 -0
  20. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/authentication.md +0 -0
  21. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/models/model_serializer.md +0 -0
  22. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/models/model_util.md +0 -0
  23. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/models/serializers.md +0 -0
  24. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/pagination.md +0 -0
  25. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/renderers/orjson_renderer.md +0 -0
  26. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/views/api_view.md +0 -0
  27. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/api/views/mixins.md +0 -0
  28. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/auth.md +0 -0
  29. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/contributing.md +0 -0
  30. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/extra.css +0 -0
  31. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  32. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  33. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  34. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  35. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  36. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  37. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/getting_started/installation.md +0 -0
  38. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/images/bar-swagger.png +0 -0
  39. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/images/favicon.ico +0 -0
  40. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/images/foo-swagger.png +0 -0
  41. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/images/logo.png +0 -0
  42. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  43. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/index.md +0 -0
  44. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/release_notes.md +0 -0
  45. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/requirements.txt +0 -0
  46. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/tutorial/authentication.md +0 -0
  47. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/tutorial/crud.md +0 -0
  48. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/tutorial/filtering.md +0 -0
  49. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/docs/tutorial/model.md +0 -0
  50. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/main.py +0 -0
  51. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/api.py +0 -0
  52. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/auth.py +0 -0
  53. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/decorators/__init__.py +0 -0
  54. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/decorators/operations.py +0 -0
  55. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/decorators/views.py +0 -0
  56. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/factory/__init__.py +0 -0
  57. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/factory/operations.py +0 -0
  58. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/helpers/__init__.py +0 -0
  59. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/helpers/api.py +0 -0
  60. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/helpers/query.py +0 -0
  61. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/models/__init__.py +0 -0
  62. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/models/utils.py +0 -0
  63. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/parsers.py +0 -0
  64. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/renders.py +0 -0
  65. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/schemas/__init__.py +0 -0
  66. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/schemas/api.py +0 -0
  67. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/schemas/filters.py +0 -0
  68. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/schemas/generics.py +0 -0
  69. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/schemas/helpers.py +0 -0
  70. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/types.py +0 -0
  71. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/views/__init__.py +0 -0
  72. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/views/api.py +0 -0
  73. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/ninja_aio/views/mixins.py +0 -0
  74. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/pyproject.toml +0 -0
  75. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/requirements.dev.txt +0 -0
  76. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/run-local-coverage.sh +0 -0
  77. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/__init__.py +0 -0
  78. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/core/__init__.py +0 -0
  79. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/core/test_decorators.py +0 -0
  80. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/core/test_exceptions_api.py +0 -0
  81. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/core/test_renderer_parser.py +0 -0
  82. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/generics/__init__.py +0 -0
  83. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/generics/literals.py +0 -0
  84. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/generics/models.py +0 -0
  85. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/generics/request.py +0 -0
  86. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/generics/views.py +0 -0
  87. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/helpers/__init__.py +0 -0
  88. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/helpers/test_many_to_many_api.py +0 -0
  89. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/models/__init__.py +0 -0
  90. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/models/test_model_util.py +0 -0
  91. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/models/test_models_extra.py +0 -0
  92. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_app/__init__.py +0 -0
  93. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_app/models.py +0 -0
  94. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_app/schema.py +0 -0
  95. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_app/serializers.py +0 -0
  96. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_app/views.py +0 -0
  97. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_auth.py +0 -0
  98. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_decorators.py +0 -0
  99. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_exceptions.py +0 -0
  100. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_query_util.py +0 -0
  101. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/test_settings.py +0 -0
  102. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/views/__init__.py +0 -0
  103. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/views/test_views.py +0 -0
  104. {django_ninja_aio_crud-2.16.0 → django_ninja_aio_crud-2.16.2}/tests/views/test_viewset.py +0 -0
@@ -28,6 +28,7 @@ on:
28
28
  - "2.13"
29
29
  - "2.14"
30
30
  - "2.15"
31
+ - "2.16"
31
32
  make_latest:
32
33
  description: 'Set as "latest" and default?'
33
34
  type: boolean
@@ -59,6 +60,7 @@ on:
59
60
  - "2.13"
60
61
  - "2.14"
61
62
  - "2.15"
63
+ - "2.16"
62
64
  delete_confirm:
63
65
  description: 'Confirm deletion of the selected version'
64
66
  type: boolean
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.16.0
3
+ Version: 2.16.2
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -294,6 +294,8 @@ Relations are declared via `M2MRelationSchema` objects (not tuples). Each schema
294
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`.
295
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.
296
296
  - `verbose_name_plural`: optional human-readable plural name for the related model, used in endpoint summaries and descriptions. When not provided, defaults to the related model's `_meta.verbose_name_plural`.
297
+ - `get_decorators`: optional list of decorators to apply to the GET (list related objects) endpoint. Decorators are applied via `decorate_view()` alongside built-in decorators like `unique_view` and `paginate`.
298
+ - `post_decorators`: optional list of decorators to apply to the POST (add/remove) endpoint. Decorators are applied via `decorate_view()` alongside the built-in `unique_view` decorator.
297
299
 
298
300
  If `path` is empty it falls back to the related model verbose name (lowercase plural).
299
301
  If `filters` is provided, a per-relation filters schema is auto-generated and exposed on the GET relation endpoint:
@@ -489,6 +491,44 @@ M2MRelationSchema(
489
491
  )
490
492
  ```
491
493
 
494
+ Example with custom decorators:
495
+
496
+ ```python
497
+ from functools import wraps
498
+
499
+ def cache_response(timeout=60):
500
+ """Cache decorator for GET endpoints."""
501
+ def decorator(func):
502
+ @wraps(func)
503
+ async def wrapper(*args, **kwargs):
504
+ # caching logic here
505
+ return await func(*args, **kwargs)
506
+ return wrapper
507
+ return decorator
508
+
509
+ def rate_limit(max_calls=100):
510
+ """Rate limiting decorator for POST endpoints."""
511
+ def decorator(func):
512
+ @wraps(func)
513
+ async def wrapper(*args, **kwargs):
514
+ # rate limiting logic here
515
+ return await func(*args, **kwargs)
516
+ return wrapper
517
+ return decorator
518
+
519
+ M2MRelationSchema(
520
+ model=Tag,
521
+ related_name="tags",
522
+ get_decorators=[cache_response(timeout=300)], # Cache GET for 5 minutes
523
+ post_decorators=[rate_limit(max_calls=50)], # Rate limit POST operations
524
+ add=True,
525
+ remove=True,
526
+ get=True,
527
+ )
528
+ ```
529
+
530
+ Note: Decorators are applied in addition to built-in decorators (`unique_view`, `paginate`). The order follows standard Python decorator stacking: decorators listed first are applied outermost.
531
+
492
532
  ## Custom Views
493
533
 
494
534
  Preferred (decorators): see the section above.
@@ -63,6 +63,23 @@ class MyViewSet(APIViewSet):
63
63
 
64
64
  These are applied in combination with built-ins (e.g., unique_view, paginate) using decorate_view in the implementation.
65
65
 
66
+ ## M2MRelationSchema decorators
67
+
68
+ Apply custom decorators to Many-to-Many relation endpoints via `get_decorators` and `post_decorators`:
69
+
70
+ ```python
71
+ from ninja_aio.schemas import M2MRelationSchema
72
+
73
+ M2MRelationSchema(
74
+ model=Tag,
75
+ related_name="tags",
76
+ get_decorators=[cache_decorator, log_decorator], # Applied to GET (list related)
77
+ post_decorators=[rate_limit_decorator], # Applied to POST (add/remove)
78
+ )
79
+ ```
80
+
81
+ These decorators are applied alongside built-in decorators (`unique_view`, `paginate`) using `decorate_view`. See [APIViewSet M2M Relations](api_view_set.md#many-to-many-relations) for more details.
82
+
66
83
  ## ApiMethodFactory.decorators
67
84
 
68
85
  Example: use api_get within a ViewSet with extra decorators:
@@ -1,8 +1,13 @@
1
- ## 🚀 Quick Start
1
+ ## 🚀 Quick Start (ModelSerializer)
2
+
3
+ This guide shows how to create a CRUD API using `ModelSerializer`, which combines your Django model and serialization configuration in a single class.
4
+
5
+ !!! tip "Alternative Approach"
6
+ If you prefer to keep your models unchanged and define serialization separately, see [Quick Start (Serializer)](quick_start_serializer.md).
2
7
 
3
8
  ### 1. Create Your Model
4
9
 
5
- Define your model using `ModelSerializer`:
10
+ Define your model using `ModelSerializer` with embedded serializer configuration:
6
11
 
7
12
  ```python
8
13
  # models.py
@@ -44,12 +49,9 @@ from .models import Article
44
49
  api = NinjaAIO(title="My Blog API", version="1.0.0")
45
50
 
46
51
 
52
+ @api.viewset(model=Article)
47
53
  class ArticleViewSet(APIViewSet):
48
- model = Article
49
- api = api
50
-
51
-
52
- ArticleViewSet().add_views_to_route()
54
+ pass
53
55
  ```
54
56
 
55
57
  ### 3. Configure URLs
@@ -0,0 +1,220 @@
1
+ ## 🚀 Quick Start (Serializer)
2
+
3
+ This guide shows how to create a CRUD API using the `Serializer` class with plain Django models. This approach keeps your models unchanged and defines API configuration separately.
4
+
5
+ !!! tip "Alternative Approach"
6
+ If you prefer an all-in-one approach with embedded serialization, see [Quick Start (ModelSerializer)](quick_start.md).
7
+
8
+ ### When to Use Serializer
9
+
10
+ Choose the `Serializer` approach when:
11
+
12
+ - You have existing Django models you don't want to modify
13
+ - You want to keep models and API concerns separated
14
+ - You're adding API functionality to an existing project
15
+ - Multiple teams work on models vs. API layers
16
+
17
+ ### 1. Create Your Model
18
+
19
+ Use a standard Django model:
20
+
21
+ ```python
22
+ # models.py
23
+ from django.db import models
24
+
25
+
26
+ class Article(models.Model):
27
+ title = models.CharField(max_length=200)
28
+ content = models.TextField()
29
+ is_published = models.BooleanField(default=False)
30
+ created_at = models.DateTimeField(auto_now_add=True)
31
+
32
+ class Meta:
33
+ ordering = ["-created_at"]
34
+ ```
35
+
36
+ ### 2. Create Your Serializer
37
+
38
+ Define a `Serializer` class with API configuration in a nested `Meta` class:
39
+
40
+ ```python
41
+ # serializers.py
42
+ from ninja_aio.models import serializers
43
+ from .models import Article
44
+
45
+
46
+ class ArticleSerializer(serializers.Serializer):
47
+ class Meta:
48
+ model = Article
49
+ schema_in = serializers.SchemaModelConfig(
50
+ fields=["title", "content"],
51
+ optionals=[("is_published", bool)],
52
+ )
53
+ schema_out = serializers.SchemaModelConfig(
54
+ fields=["id", "title", "content", "is_published", "created_at"]
55
+ )
56
+ schema_update = serializers.SchemaModelConfig(
57
+ optionals=[
58
+ ("title", str),
59
+ ("content", str),
60
+ ("is_published", bool),
61
+ ]
62
+ )
63
+ ```
64
+
65
+ ### 3. Create Your ViewSet
66
+
67
+ Define your API views using `APIViewSet` with `serializer_class`:
68
+
69
+ ```python
70
+ # views.py
71
+ from ninja_aio import NinjaAIO
72
+ from ninja_aio.views import APIViewSet
73
+ from .models import Article
74
+ from .serializers import ArticleSerializer
75
+
76
+ api = NinjaAIO(title="My Blog API", version="1.0.0")
77
+
78
+
79
+ @api.viewset(model=Article)
80
+ class ArticleViewSet(APIViewSet):
81
+ serializer_class = ArticleSerializer
82
+ ```
83
+
84
+ ### 4. Configure URLs
85
+
86
+ Add the API to your URL configuration:
87
+
88
+ ```python
89
+ # urls.py
90
+ from django.urls import path
91
+ from .views import api
92
+
93
+ urlpatterns = [
94
+ path("api/", api.urls),
95
+ ]
96
+ ```
97
+
98
+ ### 5. Run Your Server
99
+
100
+ ```bash
101
+ python manage.py runserver
102
+ ```
103
+
104
+ Visit **[http://localhost:8000/api/docs](http://localhost:8000/api/docs)** to see your auto-generated API documentation!
105
+
106
+ ## Adding Relationships
107
+
108
+ The `Serializer` approach supports nested serialization for related models:
109
+
110
+ ```python
111
+ # models.py
112
+ from django.db import models
113
+
114
+
115
+ class Author(models.Model):
116
+ name = models.CharField(max_length=200)
117
+ email = models.EmailField()
118
+
119
+
120
+ class Article(models.Model):
121
+ title = models.CharField(max_length=200)
122
+ content = models.TextField()
123
+ author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="articles")
124
+ is_published = models.BooleanField(default=False)
125
+ created_at = models.DateTimeField(auto_now_add=True)
126
+ ```
127
+
128
+ ```python
129
+ # serializers.py
130
+ from ninja_aio.models import serializers
131
+ from .models import Author, Article
132
+
133
+
134
+ class AuthorSerializer(serializers.Serializer):
135
+ class Meta:
136
+ model = Author
137
+ schema_out = serializers.SchemaModelConfig(
138
+ fields=["id", "name", "email"]
139
+ )
140
+
141
+
142
+ class ArticleSerializer(serializers.Serializer):
143
+ class Meta:
144
+ model = Article
145
+ schema_in = serializers.SchemaModelConfig(
146
+ fields=["title", "content", "author"],
147
+ )
148
+ schema_out = serializers.SchemaModelConfig(
149
+ fields=["id", "title", "content", "author", "is_published", "created_at"]
150
+ )
151
+ schema_update = serializers.SchemaModelConfig(
152
+ optionals=[
153
+ ("title", str),
154
+ ("content", str),
155
+ ("is_published", bool),
156
+ ]
157
+ )
158
+ # Nested serialization for the author field
159
+ relations_serializers = {
160
+ "author": AuthorSerializer,
161
+ }
162
+
163
+ class QuerySet:
164
+ # Optimize queries with select_related
165
+ read = serializers.ModelQuerySetSchema(
166
+ select_related=["author"]
167
+ )
168
+ ```
169
+
170
+ ## Adding Lifecycle Hooks
171
+
172
+ Add custom logic to CRUD operations using hooks:
173
+
174
+ ```python
175
+ # serializers.py
176
+ from ninja_aio.models import serializers
177
+ from .models import Article
178
+
179
+
180
+ class ArticleSerializer(serializers.Serializer):
181
+ class Meta:
182
+ model = Article
183
+ schema_in = serializers.SchemaModelConfig(
184
+ fields=["title", "content"],
185
+ customs=[("notify_subscribers", bool, False)], # Custom field
186
+ )
187
+ schema_out = serializers.SchemaModelConfig(
188
+ fields=["id", "title", "content", "is_published", "created_at"]
189
+ )
190
+ schema_update = serializers.SchemaModelConfig(
191
+ optionals=[("title", str), ("content", str), ("is_published", bool)]
192
+ )
193
+
194
+ async def custom_actions(self, payload, instance):
195
+ """Execute after field assignment, before save."""
196
+ if payload.get("notify_subscribers"):
197
+ await send_notification(instance.title)
198
+
199
+ async def post_create(self, instance):
200
+ """Execute after instance creation."""
201
+ await AuditLog.objects.acreate(
202
+ action="article_created",
203
+ article_id=instance.id
204
+ )
205
+
206
+ def before_save(self, instance):
207
+ """Execute before any save (sync hook)."""
208
+ instance.slug = slugify(instance.title)
209
+
210
+ def on_delete(self, instance):
211
+ """Execute after deletion (sync hook)."""
212
+ logger.info(f"Article {instance.id} deleted")
213
+ ```
214
+
215
+ ## Next Steps
216
+
217
+ - Learn more about [Serializer configuration](../api/models/serializers.md)
218
+ - Explore [APIViewSet features](../api/views/api_view_set.md)
219
+ - Add [Authentication](../api/authentication.md) to your API
220
+ - Configure [Pagination](../api/pagination.md)
@@ -9,7 +9,8 @@ nav:
9
9
  - Home: index.md
10
10
  - Getting Started:
11
11
  - Installation: getting_started/installation.md
12
- - Quick Start: getting_started/quick_start.md
12
+ - Quick Start (ModelSerializer): getting_started/quick_start.md
13
+ - Quick Start (Serializer): getting_started/quick_start_serializer.md
13
14
  - Tutorial:
14
15
  - 'Step 1: Define Your Model': tutorial/model.md
15
16
  - 'Step 2: Create CRUD Views': tutorial/crud.md
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.16.0"
3
+ __version__ = "2.16.2"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -78,7 +78,7 @@ def _default_error(
78
78
 
79
79
 
80
80
  def _pydantic_validation_error(
81
- request: HttpRequest, exc: ValidationError, api: type[NinjaAPI]
81
+ request: HttpRequest, exc: PydanticValidationError, api: type[NinjaAPI]
82
82
  ) -> HttpResponse:
83
83
  """Translate a pydantic ValidationError into a normalized API error response."""
84
84
  error = PydanticValidationError(exc.errors(include_input=False))
@@ -669,9 +669,18 @@ class BaseSerializer:
669
669
  customs = cls.get_custom_fields(s_type) + optionals
670
670
  excludes = cls.get_excluded_fields(s_type)
671
671
 
672
- # If no explicit fields but have optionals, use optional field names as fields
672
+ # If no explicit fields and no excludes specified
673
673
  if not fields and not excludes:
674
- fields = [f[0] for f in optionals]
674
+ if optionals:
675
+ # Use optional field names as the fields to include
676
+ fields = [f[0] for f in optionals]
677
+ elif customs:
678
+ # Only customs defined - exclude all model fields to prevent auto-inclusion
679
+ excludes = [
680
+ f.name
681
+ for f in model._meta.get_fields()
682
+ if getattr(f, "concrete", False)
683
+ ]
675
684
 
676
685
  # Only create schema if we have something to include
677
686
  if not any([fields, customs, excludes]):
@@ -1037,18 +1046,18 @@ class SchemaModelConfig(Schema):
1037
1046
  ----------
1038
1047
  fields : Optional[List[str]]
1039
1048
  Explicit model fields to include.
1040
- optionals : Optional[List[tuple[str, type]]]
1041
- Optional model fields.
1049
+ optionals : Optional[List[tuple[str, Any]]]
1050
+ Optional model fields. Type can be any valid type annotation including Union.
1042
1051
  exclude : Optional[List[str]]
1043
1052
  Model fields to exclude.
1044
- customs : Optional[List[tuple[str, type, Any]]]
1045
- Custom / synthetic fields.
1053
+ customs : Optional[List[tuple[str, Any, Any]]]
1054
+ Custom / synthetic fields. Type can be any valid type annotation including Union.
1046
1055
  """
1047
1056
 
1048
1057
  fields: Optional[List[str]] = None
1049
- optionals: Optional[List[tuple[str, type]]] = None
1058
+ optionals: Optional[List[tuple[str, Any]]] = None
1050
1059
  exclude: Optional[List[str]] = None
1051
- customs: Optional[List[tuple[str, type, Any]]] = None
1060
+ customs: Optional[List[tuple[str, Any, Any]]] = None
1052
1061
 
1053
1062
 
1054
1063
  class Serializer(BaseSerializer, metaclass=SerializerMeta):
@@ -1227,6 +1227,161 @@ class RelationsAsIdStringPKModelSerializerTestCase(TestCase):
1227
1227
  )
1228
1228
 
1229
1229
 
1230
+ @tag("serializers", "customs_only")
1231
+ class CustomsOnlySchemaTestCase(TestCase):
1232
+ """Test cases for schemas with only customs/optionals defined (no fields/excludes)."""
1233
+
1234
+ def setUp(self):
1235
+ warnings.simplefilter("ignore", UserWarning)
1236
+
1237
+ def test_serializer_create_schema_with_only_customs(self):
1238
+ """Test that create schema with only customs does NOT include model fields."""
1239
+
1240
+ class CustomsOnlyCreateSerializer(serializers.Serializer):
1241
+ class Meta:
1242
+ model = TestModelForeignKey
1243
+ schema_in = serializers.SchemaModelConfig(
1244
+ customs=[("custom_input", str, ...)]
1245
+ )
1246
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
1247
+
1248
+ schema = CustomsOnlyCreateSerializer.generate_create_s()
1249
+ self.assertIsNotNone(schema)
1250
+ # Should only have the custom field, not model fields
1251
+ self.assertIn("custom_input", schema.model_fields)
1252
+ # Should NOT have model fields auto-included
1253
+ self.assertNotIn("name", schema.model_fields)
1254
+ self.assertNotIn("description", schema.model_fields)
1255
+ self.assertNotIn("test_model", schema.model_fields)
1256
+ self.assertNotIn("id", schema.model_fields)
1257
+
1258
+ def test_serializer_update_schema_with_only_customs(self):
1259
+ """Test that update schema with only customs does NOT include model fields."""
1260
+
1261
+ class CustomsOnlyUpdateSerializer(serializers.Serializer):
1262
+ class Meta:
1263
+ model = TestModelForeignKey
1264
+ schema_update = serializers.SchemaModelConfig(
1265
+ customs=[("custom_patch_field", str, None)]
1266
+ )
1267
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
1268
+
1269
+ schema = CustomsOnlyUpdateSerializer.generate_update_s()
1270
+ self.assertIsNotNone(schema)
1271
+ # Should only have the custom field
1272
+ self.assertIn("custom_patch_field", schema.model_fields)
1273
+ # Should NOT have model fields auto-included
1274
+ self.assertNotIn("name", schema.model_fields)
1275
+ self.assertNotIn("description", schema.model_fields)
1276
+ self.assertNotIn("test_model", schema.model_fields)
1277
+ self.assertNotIn("id", schema.model_fields)
1278
+
1279
+ def test_serializer_create_schema_with_customs_and_optionals(self):
1280
+ """Test create schema with customs + optionals includes only those fields."""
1281
+
1282
+ class CustomsAndOptionalsSerializer(serializers.Serializer):
1283
+ class Meta:
1284
+ model = TestModelForeignKey
1285
+ schema_in = serializers.SchemaModelConfig(
1286
+ customs=[("custom_field", str, ...)],
1287
+ optionals=[("name", str)], # name is a model field made optional
1288
+ )
1289
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
1290
+
1291
+ schema = CustomsAndOptionalsSerializer.generate_create_s()
1292
+ self.assertIsNotNone(schema)
1293
+ # Should have custom field
1294
+ self.assertIn("custom_field", schema.model_fields)
1295
+ # Should have the optional model field
1296
+ self.assertIn("name", schema.model_fields)
1297
+ # Should NOT have other model fields
1298
+ self.assertNotIn("description", schema.model_fields)
1299
+ self.assertNotIn("test_model", schema.model_fields)
1300
+ self.assertNotIn("id", schema.model_fields)
1301
+
1302
+ def test_serializer_with_fields_still_works(self):
1303
+ """Test that defining fields still works as expected."""
1304
+
1305
+ class FieldsDefinedSerializer(serializers.Serializer):
1306
+ class Meta:
1307
+ model = TestModelForeignKey
1308
+ schema_in = serializers.SchemaModelConfig(
1309
+ fields=["name", "description"],
1310
+ customs=[("extra", int, 0)],
1311
+ )
1312
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
1313
+
1314
+ schema = FieldsDefinedSerializer.generate_create_s()
1315
+ self.assertIsNotNone(schema)
1316
+ # Should have specified fields
1317
+ self.assertIn("name", schema.model_fields)
1318
+ self.assertIn("description", schema.model_fields)
1319
+ # Should have custom field
1320
+ self.assertIn("extra", schema.model_fields)
1321
+ # Should NOT have other model fields
1322
+ self.assertNotIn("test_model", schema.model_fields)
1323
+ self.assertNotIn("id", schema.model_fields)
1324
+
1325
+ def test_serializer_with_only_excludes_and_customs(self):
1326
+ """Test schema with excludes and customs but no fields only has customs."""
1327
+
1328
+ class ExcludesOnlySerializer(serializers.Serializer):
1329
+ class Meta:
1330
+ model = TestModelForeignKey
1331
+ schema_in = serializers.SchemaModelConfig(
1332
+ exclude=["id", "test_model"],
1333
+ customs=[("extra", int, 0)],
1334
+ )
1335
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
1336
+
1337
+ schema = ExcludesOnlySerializer.generate_create_s()
1338
+ self.assertIsNotNone(schema)
1339
+ # With excludes but no fields, only customs are included
1340
+ # because fields=[] is passed to create_schema (no model fields)
1341
+ self.assertIn("extra", schema.model_fields)
1342
+ # This is expected behavior: without explicit fields, no model fields included
1343
+ self.assertEqual(len(schema.model_fields), 1)
1344
+
1345
+ def test_serializer_empty_schema_returns_none(self):
1346
+ """Test that schema with nothing defined returns None."""
1347
+
1348
+ class EmptySchemaSerializer(serializers.Serializer):
1349
+ class Meta:
1350
+ model = TestModelForeignKey
1351
+ schema_in = serializers.SchemaModelConfig()
1352
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
1353
+
1354
+ schema = EmptySchemaSerializer.generate_create_s()
1355
+ self.assertIsNone(schema)
1356
+
1357
+ def test_serializer_multiple_customs_no_model_fields(self):
1358
+ """Test schema with multiple customs but no model fields."""
1359
+
1360
+ class MultipleCustomsSerializer(serializers.Serializer):
1361
+ class Meta:
1362
+ model = TestModelForeignKey
1363
+ schema_in = serializers.SchemaModelConfig(
1364
+ customs=[
1365
+ ("custom1", str, ...),
1366
+ ("custom2", int, 0),
1367
+ ("custom3", bool, False),
1368
+ ]
1369
+ )
1370
+ schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
1371
+
1372
+ schema = MultipleCustomsSerializer.generate_create_s()
1373
+ self.assertIsNotNone(schema)
1374
+ # Should have all custom fields
1375
+ self.assertIn("custom1", schema.model_fields)
1376
+ self.assertIn("custom2", schema.model_fields)
1377
+ self.assertIn("custom3", schema.model_fields)
1378
+ # Should NOT have any model fields
1379
+ self.assertNotIn("name", schema.model_fields)
1380
+ self.assertNotIn("description", schema.model_fields)
1381
+ self.assertNotIn("test_model", schema.model_fields)
1382
+ self.assertNotIn("id", schema.model_fields)
1383
+
1384
+
1230
1385
  @tag("serializers", "relations_as_id", "string_pk", "integration")
1231
1386
  class RelationsAsIdStringPKIntegrationTestCase(TestCase):
1232
1387
  """Integration tests for relations_as_id with string primary keys and actual data serialization."""