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.
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/workflows/docs.yml +4 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/models/model_serializer.md +69 -7
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/models/serializers.md +206 -5
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/api_view_set.md +80 -20
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/helpers/api.py +1 -1
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/models/serializers.py +281 -89
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/helpers.py +32 -6
- django_ninja_aio_crud-2.8.0/ninja_aio/types.py +24 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/views/api.py +21 -7
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/views.py +1 -1
- django_ninja_aio_crud-2.8.0/tests/helpers/test_many_to_many_api.py +282 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/models.py +13 -0
- django_ninja_aio_crud-2.8.0/tests/test_serializers.py +412 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/views/test_viewset.py +141 -0
- django_ninja_aio_crud-2.6.1/ninja_aio/types.py +0 -19
- django_ninja_aio_crud-2.6.1/tests/helpers/test_many_to_many_api.py +0 -118
- django_ninja_aio_crud-2.6.1/tests/test_serializers.py +0 -106
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/README.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/extra.css +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/images/logo.png +0 -0
- {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
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/main.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/serializers.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/tests/views/__init__.py +0 -0
- {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
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.8.0}/docs/api/models/model_serializer.md
RENAMED
|
@@ -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
|
|
257
|
+
ModelSerializer automatically generates five schema types:
|
|
198
258
|
|
|
199
|
-
| Method
|
|
200
|
-
|
|
|
201
|
-
| `generate_create_s()`
|
|
202
|
-
| `generate_update_s()`
|
|
203
|
-
| `generate_read_s(depth=1)`
|
|
204
|
-
| `
|
|
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 **
|
|
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
|
|
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) |
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
@@ -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
|