django-ninja-aio-crud 2.13.0__tar.gz → 2.14.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.
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/.github/workflows/docs.yml +4 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/models/model_serializer.md +107 -5
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/models/serializers.md +129 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/models/serializers.py +128 -13
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_app/models.py +75 -0
- django_ninja_aio_crud-2.14.0/tests/test_app/serializers.py +118 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_serializers.py +313 -0
- django_ninja_aio_crud-2.13.0/tests/test_app/serializers.py +0 -47
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/README.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/views/api_view_set.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/main.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/helpers/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/schemas/filters.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/views/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/views/test_views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/tests/views/test_viewset.py +0 -0
|
@@ -25,6 +25,8 @@ on:
|
|
|
25
25
|
- "2.10"
|
|
26
26
|
- "2.11"
|
|
27
27
|
- "2.12"
|
|
28
|
+
- "2.13"
|
|
29
|
+
- "2.14"
|
|
28
30
|
make_latest:
|
|
29
31
|
description: 'Set as "latest" and default?'
|
|
30
32
|
type: boolean
|
|
@@ -53,6 +55,8 @@ on:
|
|
|
53
55
|
- "2.10"
|
|
54
56
|
- "2.11"
|
|
55
57
|
- "2.12"
|
|
58
|
+
- "2.13"
|
|
59
|
+
- "2.14"
|
|
56
60
|
delete_confirm:
|
|
57
61
|
description: 'Confirm deletion of the selected version'
|
|
58
62
|
type: boolean
|
{django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/models/model_serializer.md
RENAMED
|
@@ -100,11 +100,12 @@ Describes how to build a read (output) schema for a model.
|
|
|
100
100
|
|
|
101
101
|
**Attributes**
|
|
102
102
|
|
|
103
|
-
| Attribute
|
|
104
|
-
|
|
105
|
-
| `fields`
|
|
106
|
-
| `excludes`
|
|
107
|
-
| `customs`
|
|
103
|
+
| Attribute | Type | Description |
|
|
104
|
+
|------------------|-----------------|-------------|
|
|
105
|
+
| `fields` | `list[str]` | **REQUIRED.** Model fields / related names explicitly included in the read (output) schema. |
|
|
106
|
+
| `excludes` | `list[str]` | Fields / related names to always omit (takes precedence over `fields` and `optionals`). Use for sensitive or noisy data (e.g., passwords, internal flags). |
|
|
107
|
+
| `customs` | `list[tuple]` | Computed / synthetic output values. Tuple formats:<br>• `(name, type)` = required resolvable attribute (object attribute or property). Serialization error if not resolvable.<br>• `(name, type, default)` = optional; default may be a callable (`lambda obj: ...`) or a literal value. |
|
|
108
|
+
| `relations_as_id`| `list[str]` | Relation fields to serialize as IDs instead of nested objects. Works with forward FK, forward O2O, reverse FK, reverse O2O, and M2M relations. |
|
|
108
109
|
|
|
109
110
|
**Example:**
|
|
110
111
|
|
|
@@ -405,6 +406,107 @@ class Book(ModelSerializer):
|
|
|
405
406
|
}
|
|
406
407
|
```
|
|
407
408
|
|
|
409
|
+
#### Relations as ID
|
|
410
|
+
|
|
411
|
+
Use `relations_as_id` to serialize relation fields as IDs instead of nested objects. This is useful for:
|
|
412
|
+
|
|
413
|
+
- Reducing response payload size
|
|
414
|
+
- Avoiding circular serialization
|
|
415
|
+
- Performance optimization when nested data isn't needed
|
|
416
|
+
- API designs where clients fetch related data separately
|
|
417
|
+
|
|
418
|
+
**Supported Relations:**
|
|
419
|
+
|
|
420
|
+
| Relation Type | Output Type | Example Value |
|
|
421
|
+
|--------------------|-------------------|----------------------|
|
|
422
|
+
| Forward FK | `int \| None` | `5` or `null` |
|
|
423
|
+
| Forward O2O | `int \| None` | `3` or `null` |
|
|
424
|
+
| Reverse FK | `list[int]` | `[1, 2, 3]` |
|
|
425
|
+
| Reverse O2O | `int \| None` | `7` or `null` |
|
|
426
|
+
| M2M (forward) | `list[int]` | `[1, 2]` |
|
|
427
|
+
| M2M (reverse) | `list[int]` | `[4, 5, 6]` |
|
|
428
|
+
|
|
429
|
+
**Example:**
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
class Author(ModelSerializer):
|
|
433
|
+
name = models.CharField(max_length=200)
|
|
434
|
+
|
|
435
|
+
class ReadSerializer:
|
|
436
|
+
fields = ["id", "name", "books"]
|
|
437
|
+
relations_as_id = ["books"] # Serialize as list of IDs
|
|
438
|
+
|
|
439
|
+
class Book(ModelSerializer):
|
|
440
|
+
title = models.CharField(max_length=200)
|
|
441
|
+
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
|
|
442
|
+
|
|
443
|
+
class ReadSerializer:
|
|
444
|
+
fields = ["id", "title", "author"]
|
|
445
|
+
relations_as_id = ["author"] # Serialize as ID
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Output (Author):**
|
|
449
|
+
|
|
450
|
+
```json
|
|
451
|
+
{
|
|
452
|
+
"id": 1,
|
|
453
|
+
"name": "J.K. Rowling",
|
|
454
|
+
"books": [1, 2, 3]
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Output (Book):**
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"id": 1,
|
|
463
|
+
"title": "Harry Potter",
|
|
464
|
+
"author": 1
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**M2M Example:**
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
class Tag(ModelSerializer):
|
|
472
|
+
name = models.CharField(max_length=50)
|
|
473
|
+
|
|
474
|
+
class ReadSerializer:
|
|
475
|
+
fields = ["id", "name", "articles"]
|
|
476
|
+
relations_as_id = ["articles"] # Reverse M2M as IDs
|
|
477
|
+
|
|
478
|
+
class Article(ModelSerializer):
|
|
479
|
+
title = models.CharField(max_length=200)
|
|
480
|
+
tags = models.ManyToManyField(Tag, related_name="articles")
|
|
481
|
+
|
|
482
|
+
class ReadSerializer:
|
|
483
|
+
fields = ["id", "title", "tags"]
|
|
484
|
+
relations_as_id = ["tags"] # Forward M2M as IDs
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Output (Article):**
|
|
488
|
+
|
|
489
|
+
```json
|
|
490
|
+
{
|
|
491
|
+
"id": 1,
|
|
492
|
+
"title": "Getting Started with Django",
|
|
493
|
+
"tags": [1, 2, 5]
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Query Optimization Note:** When using `relations_as_id`, you should still use `select_related()` for forward relations and `prefetch_related()` for reverse/M2M relations to avoid N+1 queries:
|
|
498
|
+
|
|
499
|
+
```python
|
|
500
|
+
class Article(ModelSerializer):
|
|
501
|
+
# ...
|
|
502
|
+
|
|
503
|
+
class QuerySet:
|
|
504
|
+
read = ModelQuerySetSchema(
|
|
505
|
+
select_related=["author"], # For forward FK
|
|
506
|
+
prefetch_related=["tags"], # For M2M
|
|
507
|
+
)
|
|
508
|
+
```
|
|
509
|
+
|
|
408
510
|
## Async Extension Points
|
|
409
511
|
|
|
410
512
|
### `queryset_request(request)`
|
{django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/docs/api/models/serializers.md
RENAMED
|
@@ -41,6 +41,7 @@ Define a Serializer subclass with a nested Meta:
|
|
|
41
41
|
- **schema_detail**: SchemaModelConfig for detail outputs (retrieve endpoint)
|
|
42
42
|
- **schema_update**: SchemaModelConfig for patch/update inputs
|
|
43
43
|
- **relations_serializers**: Mapping of relation field name -> Serializer class, **string reference**, or **Union of serializers** (supports forward/circular dependencies and polymorphic relations)
|
|
44
|
+
- **relations_as_id**: List of relation field names to serialize as IDs instead of nested objects
|
|
44
45
|
|
|
45
46
|
SchemaModelConfig fields:
|
|
46
47
|
|
|
@@ -415,6 +416,134 @@ Notes:
|
|
|
415
416
|
- Union types are resolved lazily, so forward and circular references work seamlessly.
|
|
416
417
|
- The schema generator will create a union of all possible schemas from the serializers in the Union.
|
|
417
418
|
|
|
419
|
+
## Relations as ID
|
|
420
|
+
|
|
421
|
+
Use `relations_as_id` in Meta to serialize relation fields as IDs instead of nested objects. This is useful for:
|
|
422
|
+
|
|
423
|
+
- Reducing response payload size
|
|
424
|
+
- Avoiding circular serialization
|
|
425
|
+
- Performance optimization when nested data isn't needed
|
|
426
|
+
- API designs where clients fetch related data separately
|
|
427
|
+
|
|
428
|
+
**Supported Relations:**
|
|
429
|
+
|
|
430
|
+
| Relation Type | Output Type | Example Value |
|
|
431
|
+
|--------------------|-------------------|----------------------|
|
|
432
|
+
| Forward FK | `int \| None` | `5` or `null` |
|
|
433
|
+
| Forward O2O | `int \| None` | `3` or `null` |
|
|
434
|
+
| Reverse FK | `list[int]` | `[1, 2, 3]` |
|
|
435
|
+
| Reverse O2O | `int \| None` | `7` or `null` |
|
|
436
|
+
| M2M (forward) | `list[int]` | `[1, 2]` |
|
|
437
|
+
| M2M (reverse) | `list[int]` | `[4, 5, 6]` |
|
|
438
|
+
|
|
439
|
+
**Example:**
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
from ninja_aio.models import serializers
|
|
443
|
+
from . import models
|
|
444
|
+
|
|
445
|
+
class AuthorSerializer(serializers.Serializer):
|
|
446
|
+
class Meta:
|
|
447
|
+
model = models.Author
|
|
448
|
+
schema_out = serializers.SchemaModelConfig(
|
|
449
|
+
fields=["id", "name", "books"]
|
|
450
|
+
)
|
|
451
|
+
relations_as_id = ["books"] # Serialize reverse FK as list of IDs
|
|
452
|
+
|
|
453
|
+
class BookSerializer(serializers.Serializer):
|
|
454
|
+
class Meta:
|
|
455
|
+
model = models.Book
|
|
456
|
+
schema_out = serializers.SchemaModelConfig(
|
|
457
|
+
fields=["id", "title", "author"]
|
|
458
|
+
)
|
|
459
|
+
relations_as_id = ["author"] # Serialize forward FK as ID
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Output (Author):**
|
|
463
|
+
|
|
464
|
+
```json
|
|
465
|
+
{
|
|
466
|
+
"id": 1,
|
|
467
|
+
"name": "J.K. Rowling",
|
|
468
|
+
"books": [1, 2, 3]
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Output (Book):**
|
|
473
|
+
|
|
474
|
+
```json
|
|
475
|
+
{
|
|
476
|
+
"id": 1,
|
|
477
|
+
"title": "Harry Potter",
|
|
478
|
+
"author": 1
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**M2M Example:**
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
class TagSerializer(serializers.Serializer):
|
|
486
|
+
class Meta:
|
|
487
|
+
model = models.Tag
|
|
488
|
+
schema_out = serializers.SchemaModelConfig(
|
|
489
|
+
fields=["id", "name", "articles"]
|
|
490
|
+
)
|
|
491
|
+
relations_as_id = ["articles"] # Reverse M2M as list of IDs
|
|
492
|
+
|
|
493
|
+
class ArticleSerializer(serializers.Serializer):
|
|
494
|
+
class Meta:
|
|
495
|
+
model = models.Article
|
|
496
|
+
schema_out = serializers.SchemaModelConfig(
|
|
497
|
+
fields=["id", "title", "tags"]
|
|
498
|
+
)
|
|
499
|
+
relations_as_id = ["tags"] # Forward M2M as list of IDs
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Output (Article):**
|
|
503
|
+
|
|
504
|
+
```json
|
|
505
|
+
{
|
|
506
|
+
"id": 1,
|
|
507
|
+
"title": "Getting Started with Django",
|
|
508
|
+
"tags": [1, 2, 5]
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Combining with relations_serializers:**
|
|
513
|
+
|
|
514
|
+
You can use both `relations_as_id` and `relations_serializers` in the same serializer. Fields in `relations_as_id` take precedence:
|
|
515
|
+
|
|
516
|
+
```python
|
|
517
|
+
class ArticleSerializer(serializers.Serializer):
|
|
518
|
+
class Meta:
|
|
519
|
+
model = models.Article
|
|
520
|
+
schema_out = serializers.SchemaModelConfig(
|
|
521
|
+
fields=["id", "title", "author", "tags", "category"]
|
|
522
|
+
)
|
|
523
|
+
relations_serializers = {
|
|
524
|
+
"author": AuthorSerializer, # Nested object
|
|
525
|
+
"category": CategorySerializer, # Nested object
|
|
526
|
+
}
|
|
527
|
+
relations_as_id = ["tags"] # Just IDs
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**Query Optimization Note:** When using `relations_as_id`, you should still use `select_related()` for forward relations and `prefetch_related()` for reverse/M2M relations to avoid N+1 queries:
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
class ArticleSerializer(serializers.Serializer):
|
|
534
|
+
class Meta:
|
|
535
|
+
model = models.Article
|
|
536
|
+
schema_out = serializers.SchemaModelConfig(
|
|
537
|
+
fields=["id", "title", "author", "tags"]
|
|
538
|
+
)
|
|
539
|
+
relations_as_id = ["author", "tags"]
|
|
540
|
+
|
|
541
|
+
class QuerySet:
|
|
542
|
+
read = ModelQuerySetSchema(
|
|
543
|
+
select_related=["author"], # For forward FK
|
|
544
|
+
prefetch_related=["tags"], # For M2M
|
|
545
|
+
)
|
|
546
|
+
|
|
418
547
|
## Using with APIViewSet
|
|
419
548
|
|
|
420
549
|
You can attach a Serializer to an APIViewSet to auto-generate schemas and leverage queryset_request when present:
|
{django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.14.0}/ninja_aio/models/serializers.py
RENAMED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import (
|
|
2
|
+
Annotated,
|
|
3
|
+
Any,
|
|
4
|
+
List,
|
|
5
|
+
Literal,
|
|
6
|
+
Optional,
|
|
7
|
+
Union,
|
|
8
|
+
get_args,
|
|
9
|
+
get_origin,
|
|
10
|
+
ForwardRef,
|
|
11
|
+
)
|
|
2
12
|
import warnings
|
|
3
13
|
import sys
|
|
4
14
|
|
|
@@ -14,6 +24,7 @@ from django.db.models.fields.related_descriptors import (
|
|
|
14
24
|
ForwardManyToOneDescriptor,
|
|
15
25
|
ForwardOneToOneDescriptor,
|
|
16
26
|
)
|
|
27
|
+
from pydantic import BeforeValidator, Field
|
|
17
28
|
|
|
18
29
|
from ninja_aio.types import (
|
|
19
30
|
S_TYPES,
|
|
@@ -28,6 +39,17 @@ from ninja_aio.schemas.helpers import (
|
|
|
28
39
|
)
|
|
29
40
|
|
|
30
41
|
|
|
42
|
+
def _extract_pk(v: Any) -> Any:
|
|
43
|
+
"""Extract primary key from a model instance or return value as-is."""
|
|
44
|
+
if hasattr(v, "pk"):
|
|
45
|
+
return v.pk
|
|
46
|
+
return v
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Annotated type for extracting PK from model instances during serialization
|
|
50
|
+
PkFromModel = Annotated[int, BeforeValidator(_extract_pk)]
|
|
51
|
+
|
|
52
|
+
|
|
31
53
|
class BaseSerializer:
|
|
32
54
|
"""
|
|
33
55
|
BaseSerializer
|
|
@@ -212,6 +234,11 @@ class BaseSerializer:
|
|
|
212
234
|
# Optional in subclasses. Default to no explicit relation serializers.
|
|
213
235
|
return {}
|
|
214
236
|
|
|
237
|
+
@classmethod
|
|
238
|
+
def _get_relations_as_id(cls) -> list[str]:
|
|
239
|
+
# Optional in subclasses. Default to no relations as ID.
|
|
240
|
+
return []
|
|
241
|
+
|
|
215
242
|
@classmethod
|
|
216
243
|
def _generate_union_schema(cls, resolved_union: Any) -> Any:
|
|
217
244
|
"""
|
|
@@ -350,10 +377,25 @@ class BaseSerializer:
|
|
|
350
377
|
) or cls._is_special_field("update", field, "optionals")
|
|
351
378
|
|
|
352
379
|
@classmethod
|
|
353
|
-
def _build_schema_reverse_rel(
|
|
380
|
+
def _build_schema_reverse_rel(
|
|
381
|
+
cls, field_name: str, descriptor: Any, relations_as_id: list[str]
|
|
382
|
+
):
|
|
354
383
|
"""
|
|
355
384
|
Build a reverse relation schema component for 'Out' schema generation.
|
|
356
|
-
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
field_name : str
|
|
389
|
+
Name of the relation field.
|
|
390
|
+
descriptor : Any
|
|
391
|
+
Django field descriptor (ManyToManyDescriptor, ReverseManyToOneDescriptor, etc.).
|
|
392
|
+
relations_as_id : list[str]
|
|
393
|
+
Pre-fetched list of fields to serialize as IDs.
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
tuple | None
|
|
398
|
+
Custom field tuple for schema generation, or None to skip.
|
|
357
399
|
"""
|
|
358
400
|
# Resolve related model and cardinality
|
|
359
401
|
if isinstance(descriptor, ManyToManyDescriptor):
|
|
@@ -371,6 +413,15 @@ class BaseSerializer:
|
|
|
371
413
|
rel_model = descriptor.related.related_model
|
|
372
414
|
many = False
|
|
373
415
|
|
|
416
|
+
# Handle relations_as_id for reverse relations
|
|
417
|
+
if field_name in relations_as_id:
|
|
418
|
+
if many:
|
|
419
|
+
# For many relations, use PkFromModel to extract PKs from model instances
|
|
420
|
+
return (field_name, list[PkFromModel], Field(default_factory=list))
|
|
421
|
+
else:
|
|
422
|
+
# For single reverse relations (ReverseOneToOne), extract pk
|
|
423
|
+
return (field_name, PkFromModel | None, None)
|
|
424
|
+
|
|
374
425
|
schema = cls._resolve_relation_schema(field_name, rel_model)
|
|
375
426
|
if not schema:
|
|
376
427
|
return None
|
|
@@ -379,14 +430,34 @@ class BaseSerializer:
|
|
|
379
430
|
return (field_name, rel_schema_type | None, None)
|
|
380
431
|
|
|
381
432
|
@classmethod
|
|
382
|
-
def _build_schema_forward_rel(
|
|
433
|
+
def _build_schema_forward_rel(
|
|
434
|
+
cls, field_name: str, descriptor: Any, relations_as_id: list[str]
|
|
435
|
+
):
|
|
383
436
|
"""
|
|
384
437
|
Build a forward relation schema component for 'Out' schema generation.
|
|
385
|
-
|
|
386
|
-
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
field_name : str
|
|
442
|
+
Name of the relation field.
|
|
443
|
+
descriptor : Any
|
|
444
|
+
Django field descriptor (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor).
|
|
445
|
+
relations_as_id : list[str]
|
|
446
|
+
Pre-fetched list of fields to serialize as IDs.
|
|
447
|
+
|
|
448
|
+
Returns
|
|
449
|
+
-------
|
|
450
|
+
True | tuple | None
|
|
451
|
+
True to treat as plain field, a custom field tuple to include relation schema,
|
|
452
|
+
or None to skip entirely.
|
|
387
453
|
"""
|
|
388
454
|
rel_model = descriptor.field.related_model
|
|
389
455
|
|
|
456
|
+
# Handle relations_as_id: serialize as the raw FK ID
|
|
457
|
+
if field_name in relations_as_id:
|
|
458
|
+
# Use PkFromModel to extract pk from the related instance during serialization
|
|
459
|
+
return (field_name, PkFromModel | None, None)
|
|
460
|
+
|
|
390
461
|
# Special case: ModelSerializer with no readable fields should be skipped entirely
|
|
391
462
|
if isinstance(rel_model, ModelSerializerMeta):
|
|
392
463
|
if not (
|
|
@@ -441,23 +512,44 @@ class BaseSerializer:
|
|
|
441
512
|
field_name: str,
|
|
442
513
|
model,
|
|
443
514
|
relations_serializers: dict,
|
|
515
|
+
relations_as_id: list[str],
|
|
444
516
|
) -> tuple[str | None, tuple | None, tuple | None]:
|
|
445
517
|
"""
|
|
446
518
|
Process a single field and determine its classification.
|
|
447
519
|
|
|
448
|
-
|
|
520
|
+
Parameters
|
|
521
|
+
----------
|
|
522
|
+
field_name : str
|
|
523
|
+
Name of the field to process.
|
|
524
|
+
model : Model
|
|
525
|
+
Django model class.
|
|
526
|
+
relations_serializers : dict
|
|
527
|
+
Mapping of relation field names to serializer classes.
|
|
528
|
+
relations_as_id : list[str]
|
|
529
|
+
Pre-fetched list of fields to serialize as IDs.
|
|
530
|
+
|
|
531
|
+
Returns
|
|
532
|
+
-------
|
|
533
|
+
tuple
|
|
449
534
|
(plain_field, reverse_rel, forward_rel) - only one will be non-None
|
|
450
535
|
"""
|
|
451
536
|
field_obj = getattr(model, field_name)
|
|
452
537
|
|
|
453
538
|
if cls._is_reverse_relation(field_obj):
|
|
454
|
-
if
|
|
539
|
+
if (
|
|
540
|
+
field_name not in relations_serializers
|
|
541
|
+
and field_name not in relations_as_id
|
|
542
|
+
):
|
|
455
543
|
cls._warn_missing_relation_serializer(field_name, model)
|
|
456
|
-
rel_tuple = cls._build_schema_reverse_rel(
|
|
544
|
+
rel_tuple = cls._build_schema_reverse_rel(
|
|
545
|
+
field_name, field_obj, relations_as_id
|
|
546
|
+
)
|
|
457
547
|
return (None, rel_tuple, None)
|
|
458
548
|
|
|
459
549
|
if cls._is_forward_relation(field_obj):
|
|
460
|
-
rel_tuple = cls._build_schema_forward_rel(
|
|
550
|
+
rel_tuple = cls._build_schema_forward_rel(
|
|
551
|
+
field_name, field_obj, relations_as_id
|
|
552
|
+
)
|
|
461
553
|
if rel_tuple is True:
|
|
462
554
|
return (field_name, None, None)
|
|
463
555
|
return (None, None, rel_tuple)
|
|
@@ -469,8 +561,15 @@ class BaseSerializer:
|
|
|
469
561
|
"""
|
|
470
562
|
Collect components for output schema generation (Out or Detail).
|
|
471
563
|
|
|
472
|
-
|
|
473
|
-
|
|
564
|
+
Parameters
|
|
565
|
+
----------
|
|
566
|
+
schema_type : Literal["Out", "Detail"]
|
|
567
|
+
Type of schema to generate.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
tuple
|
|
572
|
+
(fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
|
|
474
573
|
"""
|
|
475
574
|
if schema_type not in ("Out", "Detail"):
|
|
476
575
|
raise ValueError(
|
|
@@ -480,6 +579,8 @@ class BaseSerializer:
|
|
|
480
579
|
fields_type = "read" if schema_type == "Out" else "detail"
|
|
481
580
|
model = cls._get_model()
|
|
482
581
|
relations_serializers = cls._get_relations_serializers() or {}
|
|
582
|
+
# Fetch once to avoid repeated method calls during field processing
|
|
583
|
+
relations_as_id = cls._get_relations_as_id()
|
|
483
584
|
|
|
484
585
|
fields: list[str] = []
|
|
485
586
|
reverse_rels: list[tuple] = []
|
|
@@ -487,7 +588,7 @@ class BaseSerializer:
|
|
|
487
588
|
|
|
488
589
|
for field_name in cls.get_fields(fields_type):
|
|
489
590
|
plain, reverse, forward = cls._process_field(
|
|
490
|
-
field_name, model, relations_serializers
|
|
591
|
+
field_name, model, relations_serializers, relations_as_id
|
|
491
592
|
)
|
|
492
593
|
if plain:
|
|
493
594
|
fields.append(plain)
|
|
@@ -701,12 +802,15 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
701
802
|
Computed / synthetic output attributes.
|
|
702
803
|
optionals : list[tuple[str, type]]
|
|
703
804
|
Optional output fields.
|
|
805
|
+
relations_as_id : list[str]
|
|
806
|
+
Relation fields to serialize as IDs instead of nested objects.
|
|
704
807
|
"""
|
|
705
808
|
|
|
706
809
|
fields: list[str] = []
|
|
707
810
|
customs: list[tuple[str, type, Any]] = []
|
|
708
811
|
optionals: list[tuple[str, type]] = []
|
|
709
812
|
excludes: list[str] = []
|
|
813
|
+
relations_as_id: list[str] = []
|
|
710
814
|
|
|
711
815
|
class DetailSerializer:
|
|
712
816
|
"""Configuration describing detail (single object) read schema.
|
|
@@ -756,6 +860,11 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
756
860
|
"detail": "DetailSerializer",
|
|
757
861
|
}
|
|
758
862
|
|
|
863
|
+
@classmethod
|
|
864
|
+
def _get_relations_as_id(cls) -> list[str]:
|
|
865
|
+
"""Return relation fields to serialize as IDs instead of nested objects."""
|
|
866
|
+
return getattr(cls.ReadSerializer, "relations_as_id", [])
|
|
867
|
+
|
|
759
868
|
@classmethod
|
|
760
869
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
761
870
|
"""
|
|
@@ -960,6 +1069,12 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
960
1069
|
schema_update: Optional[SchemaModelConfig] = None
|
|
961
1070
|
schema_detail: Optional[SchemaModelConfig] = None
|
|
962
1071
|
relations_serializers: dict[str, "Serializer"] = {}
|
|
1072
|
+
relations_as_id: list[str] = []
|
|
1073
|
+
|
|
1074
|
+
@classmethod
|
|
1075
|
+
def _get_relations_as_id(cls) -> list[str]:
|
|
1076
|
+
relations_as_id = cls._get_meta_data("relations_as_id")
|
|
1077
|
+
return relations_as_id or []
|
|
963
1078
|
|
|
964
1079
|
@classmethod
|
|
965
1080
|
def _get_meta_data(cls, attr_name: str) -> Any:
|
|
@@ -223,3 +223,78 @@ class TestModelSerializerWithBothSerializers(BaseTestModelSerializer):
|
|
|
223
223
|
class DetailSerializer:
|
|
224
224
|
fields = ["id", "name", "description"]
|
|
225
225
|
# No customs defined - should NOT inherit from read
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ==========================================================
|
|
229
|
+
# RELATIONS AS ID TEST MODELS
|
|
230
|
+
# ==========================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class AuthorAsId(BaseTestModelSerializer):
|
|
234
|
+
"""Author model for testing relations_as_id on reverse FK."""
|
|
235
|
+
|
|
236
|
+
class ReadSerializer:
|
|
237
|
+
fields = ["id", "name", "description", "books_as_id"]
|
|
238
|
+
relations_as_id = ["books_as_id"]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class BookAsId(BaseTestModelSerializer):
|
|
242
|
+
"""Book model for testing relations_as_id on forward FK."""
|
|
243
|
+
|
|
244
|
+
author_as_id = models.ForeignKey(
|
|
245
|
+
AuthorAsId,
|
|
246
|
+
on_delete=models.CASCADE,
|
|
247
|
+
related_name="books_as_id",
|
|
248
|
+
null=True,
|
|
249
|
+
blank=True,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
class ReadSerializer:
|
|
253
|
+
fields = ["id", "name", "description", "author_as_id"]
|
|
254
|
+
relations_as_id = ["author_as_id"]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ProfileAsId(BaseTestModelSerializer):
|
|
258
|
+
"""Profile model for testing relations_as_id on reverse O2O."""
|
|
259
|
+
|
|
260
|
+
class ReadSerializer:
|
|
261
|
+
fields = ["id", "name", "description", "user_profile_as_id"]
|
|
262
|
+
relations_as_id = ["user_profile_as_id"]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class UserAsId(BaseTestModelSerializer):
|
|
266
|
+
"""User model for testing relations_as_id on forward O2O."""
|
|
267
|
+
|
|
268
|
+
profile_as_id = models.OneToOneField(
|
|
269
|
+
ProfileAsId,
|
|
270
|
+
on_delete=models.CASCADE,
|
|
271
|
+
related_name="user_profile_as_id",
|
|
272
|
+
null=True,
|
|
273
|
+
blank=True,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
class ReadSerializer:
|
|
277
|
+
fields = ["id", "name", "description", "profile_as_id"]
|
|
278
|
+
relations_as_id = ["profile_as_id"]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class TagAsId(BaseTestModelSerializer):
|
|
282
|
+
"""Tag model for testing relations_as_id on reverse M2M."""
|
|
283
|
+
|
|
284
|
+
class ReadSerializer:
|
|
285
|
+
fields = ["id", "name", "description", "articles_as_id"]
|
|
286
|
+
relations_as_id = ["articles_as_id"]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class ArticleAsId(BaseTestModelSerializer):
|
|
290
|
+
"""Article model for testing relations_as_id on forward M2M."""
|
|
291
|
+
|
|
292
|
+
tags_as_id = models.ManyToManyField(
|
|
293
|
+
TagAsId,
|
|
294
|
+
related_name="articles_as_id",
|
|
295
|
+
blank=True,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
class ReadSerializer:
|
|
299
|
+
fields = ["id", "name", "description", "tags_as_id"]
|
|
300
|
+
relations_as_id = ["tags_as_id"]
|