django-ninja-aio-crud 2.13.0__tar.gz → 2.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-ninja-aio-crud might be problematic. Click here for more details.

Files changed (106) hide show
  1. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/docs.yml +6 -0
  2. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/model_serializer.md +159 -5
  4. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/serializers.md +165 -0
  5. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/__init__.py +1 -1
  6. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/serializers.py +147 -13
  7. django_ninja_aio_crud-2.15.0/tests/test_app/models.py +478 -0
  8. django_ninja_aio_crud-2.15.0/tests/test_app/serializers.py +118 -0
  9. django_ninja_aio_crud-2.15.0/tests/test_serializers.py +1353 -0
  10. django_ninja_aio_crud-2.13.0/tests/test_app/models.py +0 -225
  11. django_ninja_aio_crud-2.13.0/tests/test_app/serializers.py +0 -47
  12. django_ninja_aio_crud-2.13.0/tests/test_serializers.py +0 -600
  13. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/dependabot.yml +0 -0
  14. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/coverage.yml +0 -0
  15. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/publish.yml +0 -0
  16. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.gitignore +0 -0
  17. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.pre-commit-config.yaml +0 -0
  18. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/LICENSE +0 -0
  19. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/README.md +0 -0
  20. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/CNAME +0 -0
  21. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/authentication.md +0 -0
  22. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/model_util.md +0 -0
  23. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/pagination.md +0 -0
  24. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/renderers/orjson_renderer.md +0 -0
  25. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/api_view.md +0 -0
  26. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/api_view_set.md +0 -0
  27. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/decorators.md +0 -0
  28. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/mixins.md +0 -0
  29. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/auth.md +0 -0
  30. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/contributing.md +0 -0
  31. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/extra.css +0 -0
  32. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  33. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  34. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  35. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  36. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  37. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  38. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/installation.md +0 -0
  39. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/quick_start.md +0 -0
  40. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/bar-swagger.png +0 -0
  41. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/favicon.ico +0 -0
  42. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/foo-swagger.png +0 -0
  43. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/logo.png +0 -0
  44. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  45. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/index.md +0 -0
  46. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/release_notes.md +0 -0
  47. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/requirements.txt +0 -0
  48. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/authentication.md +0 -0
  49. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/crud.md +0 -0
  50. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/filtering.md +0 -0
  51. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/model.md +0 -0
  52. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/main.py +0 -0
  53. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/mkdocs.yml +0 -0
  54. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/api.py +0 -0
  55. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/auth.py +0 -0
  56. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/__init__.py +0 -0
  57. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/operations.py +0 -0
  58. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/views.py +0 -0
  59. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/exceptions.py +0 -0
  60. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/factory/__init__.py +0 -0
  61. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/factory/operations.py +0 -0
  62. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/__init__.py +0 -0
  63. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/api.py +0 -0
  64. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/query.py +0 -0
  65. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/__init__.py +0 -0
  66. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/utils.py +0 -0
  67. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/parsers.py +0 -0
  68. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/renders.py +0 -0
  69. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/__init__.py +0 -0
  70. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/api.py +0 -0
  71. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/filters.py +0 -0
  72. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/generics.py +0 -0
  73. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/helpers.py +0 -0
  74. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/types.py +0 -0
  75. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/__init__.py +0 -0
  76. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/api.py +0 -0
  77. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/mixins.py +0 -0
  78. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/pyproject.toml +0 -0
  79. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/requirements.dev.txt +0 -0
  80. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/run-local-coverage.sh +0 -0
  81. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/__init__.py +0 -0
  82. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/__init__.py +0 -0
  83. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_decorators.py +0 -0
  84. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_exceptions_api.py +0 -0
  85. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_renderer_parser.py +0 -0
  86. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/__init__.py +0 -0
  87. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/literals.py +0 -0
  88. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/models.py +0 -0
  89. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/request.py +0 -0
  90. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/views.py +0 -0
  91. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/helpers/__init__.py +0 -0
  92. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/helpers/test_many_to_many_api.py +0 -0
  93. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/models/__init__.py +0 -0
  94. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/models/test_model_util.py +0 -0
  95. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/models/test_models_extra.py +0 -0
  96. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/__init__.py +0 -0
  97. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/schema.py +0 -0
  98. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/views.py +0 -0
  99. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_auth.py +0 -0
  100. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_decorators.py +0 -0
  101. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_exceptions.py +0 -0
  102. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_query_util.py +0 -0
  103. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_settings.py +0 -0
  104. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/views/__init__.py +0 -0
  105. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/views/test_views.py +0 -0
  106. {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/views/test_viewset.py +0 -0
@@ -25,6 +25,9 @@ on:
25
25
  - "2.10"
26
26
  - "2.11"
27
27
  - "2.12"
28
+ - "2.13"
29
+ - "2.14"
30
+ - "2.15"
28
31
  make_latest:
29
32
  description: 'Set as "latest" and default?'
30
33
  type: boolean
@@ -53,6 +56,9 @@ on:
53
56
  - "2.10"
54
57
  - "2.11"
55
58
  - "2.12"
59
+ - "2.13"
60
+ - "2.14"
61
+ - "2.15"
56
62
  delete_confirm:
57
63
  description: 'Confirm deletion of the selected version'
58
64
  type: boolean
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.13.0
3
+ Version: 2.15.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -100,11 +100,12 @@ Describes how to build a read (output) schema for a model.
100
100
 
101
101
  **Attributes**
102
102
 
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. |
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,159 @@ 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 | `PK_TYPE \| None` | `5` or `null` |
423
+ | Forward O2O | `PK_TYPE \| None` | `3` or `null` |
424
+ | Reverse FK | `list[PK_TYPE]` | `[1, 2, 3]` |
425
+ | Reverse O2O | `PK_TYPE \| None` | `7` or `null` |
426
+ | M2M (forward) | `list[PK_TYPE]` | `[1, 2]` |
427
+ | M2M (reverse) | `list[PK_TYPE]` | `[4, 5, 6]` |
428
+
429
+ **Note:** `PK_TYPE` is automatically detected from the related model's primary key field. Supported types include `int` (default), `UUID`, `str`, and any other Django primary key type.
430
+
431
+ **Example:**
432
+
433
+ ```python
434
+ class Author(ModelSerializer):
435
+ name = models.CharField(max_length=200)
436
+
437
+ class ReadSerializer:
438
+ fields = ["id", "name", "books"]
439
+ relations_as_id = ["books"] # Serialize as list of IDs
440
+
441
+ class Book(ModelSerializer):
442
+ title = models.CharField(max_length=200)
443
+ author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
444
+
445
+ class ReadSerializer:
446
+ fields = ["id", "title", "author"]
447
+ relations_as_id = ["author"] # Serialize as ID
448
+ ```
449
+
450
+ **Output (Author):**
451
+
452
+ ```json
453
+ {
454
+ "id": 1,
455
+ "name": "J.K. Rowling",
456
+ "books": [1, 2, 3]
457
+ }
458
+ ```
459
+
460
+ **Output (Book):**
461
+
462
+ ```json
463
+ {
464
+ "id": 1,
465
+ "title": "Harry Potter",
466
+ "author": 1
467
+ }
468
+ ```
469
+
470
+ **M2M Example:**
471
+
472
+ ```python
473
+ class Tag(ModelSerializer):
474
+ name = models.CharField(max_length=50)
475
+
476
+ class ReadSerializer:
477
+ fields = ["id", "name", "articles"]
478
+ relations_as_id = ["articles"] # Reverse M2M as IDs
479
+
480
+ class Article(ModelSerializer):
481
+ title = models.CharField(max_length=200)
482
+ tags = models.ManyToManyField(Tag, related_name="articles")
483
+
484
+ class ReadSerializer:
485
+ fields = ["id", "title", "tags"]
486
+ relations_as_id = ["tags"] # Forward M2M as IDs
487
+ ```
488
+
489
+ **Output (Article):**
490
+
491
+ ```json
492
+ {
493
+ "id": 1,
494
+ "title": "Getting Started with Django",
495
+ "tags": [1, 2, 5]
496
+ }
497
+ ```
498
+
499
+ **UUID Primary Key Example:**
500
+
501
+ When models use UUID primary keys, the output type is automatically `UUID`:
502
+
503
+ ```python
504
+ import uuid
505
+ from django.db import models
506
+ from ninja_aio.models import ModelSerializer
507
+
508
+ class Author(ModelSerializer):
509
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
510
+ name = models.CharField(max_length=200)
511
+
512
+ class ReadSerializer:
513
+ fields = ["id", "name", "books"]
514
+ relations_as_id = ["books"]
515
+
516
+ class Book(ModelSerializer):
517
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
518
+ title = models.CharField(max_length=200)
519
+ author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
520
+
521
+ class ReadSerializer:
522
+ fields = ["id", "title", "author"]
523
+ relations_as_id = ["author"]
524
+ ```
525
+
526
+ **Output (Author with UUID):**
527
+
528
+ ```json
529
+ {
530
+ "id": "550e8400-e29b-41d4-a716-446655440000",
531
+ "name": "J.K. Rowling",
532
+ "books": [
533
+ "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
534
+ "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
535
+ ]
536
+ }
537
+ ```
538
+
539
+ **Output (Book with UUID):**
540
+
541
+ ```json
542
+ {
543
+ "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
544
+ "title": "Harry Potter",
545
+ "author": "550e8400-e29b-41d4-a716-446655440000"
546
+ }
547
+ ```
548
+
549
+ **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:
550
+
551
+ ```python
552
+ class Article(ModelSerializer):
553
+ # ...
554
+
555
+ class QuerySet:
556
+ read = ModelQuerySetSchema(
557
+ select_related=["author"], # For forward FK
558
+ prefetch_related=["tags"], # For M2M
559
+ )
560
+ ```
561
+
408
562
  ## Async Extension Points
409
563
 
410
564
  ### `queryset_request(request)`
@@ -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,170 @@ 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 | `PK_TYPE \| None` | `5` or `null` |
433
+ | Forward O2O | `PK_TYPE \| None` | `3` or `null` |
434
+ | Reverse FK | `list[PK_TYPE]` | `[1, 2, 3]` |
435
+ | Reverse O2O | `PK_TYPE \| None` | `7` or `null` |
436
+ | M2M (forward) | `list[PK_TYPE]` | `[1, 2]` |
437
+ | M2M (reverse) | `list[PK_TYPE]` | `[4, 5, 6]` |
438
+
439
+ **Note:** `PK_TYPE` is automatically detected from the related model's primary key field. Supported types include `int` (default), `UUID`, `str`, and any other Django primary key type.
440
+
441
+ **Example:**
442
+
443
+ ```python
444
+ from ninja_aio.models import serializers
445
+ from . import models
446
+
447
+ class AuthorSerializer(serializers.Serializer):
448
+ class Meta:
449
+ model = models.Author
450
+ schema_out = serializers.SchemaModelConfig(
451
+ fields=["id", "name", "books"]
452
+ )
453
+ relations_as_id = ["books"] # Serialize reverse FK as list of IDs
454
+
455
+ class BookSerializer(serializers.Serializer):
456
+ class Meta:
457
+ model = models.Book
458
+ schema_out = serializers.SchemaModelConfig(
459
+ fields=["id", "title", "author"]
460
+ )
461
+ relations_as_id = ["author"] # Serialize forward FK as ID
462
+ ```
463
+
464
+ **Output (Author):**
465
+
466
+ ```json
467
+ {
468
+ "id": 1,
469
+ "name": "J.K. Rowling",
470
+ "books": [1, 2, 3]
471
+ }
472
+ ```
473
+
474
+ **Output (Book):**
475
+
476
+ ```json
477
+ {
478
+ "id": 1,
479
+ "title": "Harry Potter",
480
+ "author": 1
481
+ }
482
+ ```
483
+
484
+ **M2M Example:**
485
+
486
+ ```python
487
+ class TagSerializer(serializers.Serializer):
488
+ class Meta:
489
+ model = models.Tag
490
+ schema_out = serializers.SchemaModelConfig(
491
+ fields=["id", "name", "articles"]
492
+ )
493
+ relations_as_id = ["articles"] # Reverse M2M as list of IDs
494
+
495
+ class ArticleSerializer(serializers.Serializer):
496
+ class Meta:
497
+ model = models.Article
498
+ schema_out = serializers.SchemaModelConfig(
499
+ fields=["id", "title", "tags"]
500
+ )
501
+ relations_as_id = ["tags"] # Forward M2M as list of IDs
502
+ ```
503
+
504
+ **Output (Article):**
505
+
506
+ ```json
507
+ {
508
+ "id": 1,
509
+ "title": "Getting Started with Django",
510
+ "tags": [1, 2, 5]
511
+ }
512
+ ```
513
+
514
+ **UUID Primary Key Example:**
515
+
516
+ When related models use UUID primary keys, the output type is automatically `UUID`:
517
+
518
+ ```python
519
+ import uuid
520
+ from django.db import models
521
+
522
+ class Author(models.Model):
523
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
524
+ name = models.CharField(max_length=200)
525
+
526
+ class AuthorSerializer(serializers.Serializer):
527
+ class Meta:
528
+ model = Author
529
+ schema_out = serializers.SchemaModelConfig(
530
+ fields=["id", "name", "books"]
531
+ )
532
+ relations_as_id = ["books"]
533
+ ```
534
+
535
+ **Output (Author with UUID):**
536
+
537
+ ```json
538
+ {
539
+ "id": "550e8400-e29b-41d4-a716-446655440000",
540
+ "name": "J.K. Rowling",
541
+ "books": [
542
+ "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
543
+ "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
544
+ ]
545
+ }
546
+ ```
547
+
548
+ **Combining with relations_serializers:**
549
+
550
+ You can use both `relations_as_id` and `relations_serializers` in the same serializer. Fields in `relations_as_id` take precedence:
551
+
552
+ ```python
553
+ class ArticleSerializer(serializers.Serializer):
554
+ class Meta:
555
+ model = models.Article
556
+ schema_out = serializers.SchemaModelConfig(
557
+ fields=["id", "title", "author", "tags", "category"]
558
+ )
559
+ relations_serializers = {
560
+ "author": AuthorSerializer, # Nested object
561
+ "category": CategorySerializer, # Nested object
562
+ }
563
+ relations_as_id = ["tags"] # Just IDs
564
+ ```
565
+
566
+ **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:
567
+
568
+ ```python
569
+ class ArticleSerializer(serializers.Serializer):
570
+ class Meta:
571
+ model = models.Article
572
+ schema_out = serializers.SchemaModelConfig(
573
+ fields=["id", "title", "author", "tags"]
574
+ )
575
+ relations_as_id = ["author", "tags"]
576
+
577
+ class QuerySet:
578
+ read = ModelQuerySetSchema(
579
+ select_related=["author"], # For forward FK
580
+ prefetch_related=["tags"], # For M2M
581
+ )
582
+
418
583
  ## Using with APIViewSet
419
584
 
420
585
  You can attach a Serializer to an APIViewSet to auto-generate schemas and leverage queryset_request when present:
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.13.0"
3
+ __version__ = "2.15.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,4 +1,14 @@
1
- from typing import Any, List, Literal, Optional, Union, get_args, get_origin, ForwardRef
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,32 @@ 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
+ class PkFromModel:
50
+ """Subscriptable type for extracting PK from model instances during serialization.
51
+
52
+ Usage:
53
+ PkFromModel[int] -> for integer PKs
54
+ PkFromModel[str] -> for string PKs
55
+ PkFromModel[UUID] -> for UUID PKs
56
+ PkFromModel -> defaults to int (backwards compatible)
57
+ """
58
+
59
+ _default = Annotated[int, BeforeValidator(_extract_pk)]
60
+
61
+ def __class_getitem__(cls, pk_type: type) -> type:
62
+ return Annotated[pk_type, BeforeValidator(_extract_pk)]
63
+
64
+ def __new__(cls):
65
+ return cls._default
66
+
67
+
31
68
  class BaseSerializer:
32
69
  """
33
70
  BaseSerializer
@@ -212,6 +249,11 @@ class BaseSerializer:
212
249
  # Optional in subclasses. Default to no explicit relation serializers.
213
250
  return {}
214
251
 
252
+ @classmethod
253
+ def _get_relations_as_id(cls) -> list[str]:
254
+ # Optional in subclasses. Default to no relations as ID.
255
+ return []
256
+
215
257
  @classmethod
216
258
  def _generate_union_schema(cls, resolved_union: Any) -> Any:
217
259
  """
@@ -350,10 +392,25 @@ class BaseSerializer:
350
392
  ) or cls._is_special_field("update", field, "optionals")
351
393
 
352
394
  @classmethod
353
- def _build_schema_reverse_rel(cls, field_name: str, descriptor: Any):
395
+ def _build_schema_reverse_rel(
396
+ cls, field_name: str, descriptor: Any, relations_as_id: list[str]
397
+ ):
354
398
  """
355
399
  Build a reverse relation schema component for 'Out' schema generation.
356
- Returns a custom field tuple or None to skip.
400
+
401
+ Parameters
402
+ ----------
403
+ field_name : str
404
+ Name of the relation field.
405
+ descriptor : Any
406
+ Django field descriptor (ManyToManyDescriptor, ReverseManyToOneDescriptor, etc.).
407
+ relations_as_id : list[str]
408
+ Pre-fetched list of fields to serialize as IDs.
409
+
410
+ Returns
411
+ -------
412
+ tuple | None
413
+ Custom field tuple for schema generation, or None to skip.
357
414
  """
358
415
  # Resolve related model and cardinality
359
416
  if isinstance(descriptor, ManyToManyDescriptor):
@@ -371,6 +428,17 @@ class BaseSerializer:
371
428
  rel_model = descriptor.related.related_model
372
429
  many = False
373
430
 
431
+ # Handle relations_as_id for reverse relations
432
+ if field_name in relations_as_id:
433
+ from ninja_aio.models.utils import ModelUtil
434
+ pk_field_type = ModelUtil(rel_model).pk_field_type
435
+ if many:
436
+ # For many relations, use PkFromModel to extract PKs from model instances
437
+ return (field_name, list[PkFromModel[pk_field_type]], Field(default_factory=list))
438
+ else:
439
+ # For single reverse relations (ReverseOneToOne), extract pk
440
+ return (field_name, PkFromModel[pk_field_type] | None, None)
441
+
374
442
  schema = cls._resolve_relation_schema(field_name, rel_model)
375
443
  if not schema:
376
444
  return None
@@ -379,14 +447,36 @@ class BaseSerializer:
379
447
  return (field_name, rel_schema_type | None, None)
380
448
 
381
449
  @classmethod
382
- def _build_schema_forward_rel(cls, field_name: str, descriptor: Any):
450
+ def _build_schema_forward_rel(
451
+ cls, field_name: str, descriptor: Any, relations_as_id: list[str]
452
+ ):
383
453
  """
384
454
  Build a forward relation schema component for 'Out' schema generation.
385
- Returns True to treat as plain field, a custom field tuple to include relation schema,
386
- or None to skip entirely.
455
+
456
+ Parameters
457
+ ----------
458
+ field_name : str
459
+ Name of the relation field.
460
+ descriptor : Any
461
+ Django field descriptor (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor).
462
+ relations_as_id : list[str]
463
+ Pre-fetched list of fields to serialize as IDs.
464
+
465
+ Returns
466
+ -------
467
+ True | tuple | None
468
+ True to treat as plain field, a custom field tuple to include relation schema,
469
+ or None to skip entirely.
387
470
  """
388
471
  rel_model = descriptor.field.related_model
389
472
 
473
+ # Handle relations_as_id: serialize as the raw FK ID
474
+ if field_name in relations_as_id:
475
+ from ninja_aio.models.utils import ModelUtil
476
+ pk_field_type = ModelUtil(rel_model).pk_field_type
477
+ # Use PkFromModel to extract pk from the related instance during serialization
478
+ return (field_name, PkFromModel[pk_field_type] | None, None)
479
+
390
480
  # Special case: ModelSerializer with no readable fields should be skipped entirely
391
481
  if isinstance(rel_model, ModelSerializerMeta):
392
482
  if not (
@@ -441,23 +531,44 @@ class BaseSerializer:
441
531
  field_name: str,
442
532
  model,
443
533
  relations_serializers: dict,
534
+ relations_as_id: list[str],
444
535
  ) -> tuple[str | None, tuple | None, tuple | None]:
445
536
  """
446
537
  Process a single field and determine its classification.
447
538
 
448
- Returns:
539
+ Parameters
540
+ ----------
541
+ field_name : str
542
+ Name of the field to process.
543
+ model : Model
544
+ Django model class.
545
+ relations_serializers : dict
546
+ Mapping of relation field names to serializer classes.
547
+ relations_as_id : list[str]
548
+ Pre-fetched list of fields to serialize as IDs.
549
+
550
+ Returns
551
+ -------
552
+ tuple
449
553
  (plain_field, reverse_rel, forward_rel) - only one will be non-None
450
554
  """
451
555
  field_obj = getattr(model, field_name)
452
556
 
453
557
  if cls._is_reverse_relation(field_obj):
454
- if field_name not in relations_serializers:
558
+ if (
559
+ field_name not in relations_serializers
560
+ and field_name not in relations_as_id
561
+ ):
455
562
  cls._warn_missing_relation_serializer(field_name, model)
456
- rel_tuple = cls._build_schema_reverse_rel(field_name, field_obj)
563
+ rel_tuple = cls._build_schema_reverse_rel(
564
+ field_name, field_obj, relations_as_id
565
+ )
457
566
  return (None, rel_tuple, None)
458
567
 
459
568
  if cls._is_forward_relation(field_obj):
460
- rel_tuple = cls._build_schema_forward_rel(field_name, field_obj)
569
+ rel_tuple = cls._build_schema_forward_rel(
570
+ field_name, field_obj, relations_as_id
571
+ )
461
572
  if rel_tuple is True:
462
573
  return (field_name, None, None)
463
574
  return (None, None, rel_tuple)
@@ -469,8 +580,15 @@ class BaseSerializer:
469
580
  """
470
581
  Collect components for output schema generation (Out or Detail).
471
582
 
472
- Returns:
473
- tuple: (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
583
+ Parameters
584
+ ----------
585
+ schema_type : Literal["Out", "Detail"]
586
+ Type of schema to generate.
587
+
588
+ Returns
589
+ -------
590
+ tuple
591
+ (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
474
592
  """
475
593
  if schema_type not in ("Out", "Detail"):
476
594
  raise ValueError(
@@ -480,6 +598,8 @@ class BaseSerializer:
480
598
  fields_type = "read" if schema_type == "Out" else "detail"
481
599
  model = cls._get_model()
482
600
  relations_serializers = cls._get_relations_serializers() or {}
601
+ # Fetch once to avoid repeated method calls during field processing
602
+ relations_as_id = cls._get_relations_as_id()
483
603
 
484
604
  fields: list[str] = []
485
605
  reverse_rels: list[tuple] = []
@@ -487,7 +607,7 @@ class BaseSerializer:
487
607
 
488
608
  for field_name in cls.get_fields(fields_type):
489
609
  plain, reverse, forward = cls._process_field(
490
- field_name, model, relations_serializers
610
+ field_name, model, relations_serializers, relations_as_id
491
611
  )
492
612
  if plain:
493
613
  fields.append(plain)
@@ -701,12 +821,15 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
701
821
  Computed / synthetic output attributes.
702
822
  optionals : list[tuple[str, type]]
703
823
  Optional output fields.
824
+ relations_as_id : list[str]
825
+ Relation fields to serialize as IDs instead of nested objects.
704
826
  """
705
827
 
706
828
  fields: list[str] = []
707
829
  customs: list[tuple[str, type, Any]] = []
708
830
  optionals: list[tuple[str, type]] = []
709
831
  excludes: list[str] = []
832
+ relations_as_id: list[str] = []
710
833
 
711
834
  class DetailSerializer:
712
835
  """Configuration describing detail (single object) read schema.
@@ -756,6 +879,11 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
756
879
  "detail": "DetailSerializer",
757
880
  }
758
881
 
882
+ @classmethod
883
+ def _get_relations_as_id(cls) -> list[str]:
884
+ """Return relation fields to serialize as IDs instead of nested objects."""
885
+ return getattr(cls.ReadSerializer, "relations_as_id", [])
886
+
759
887
  @classmethod
760
888
  def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
761
889
  """
@@ -960,6 +1088,12 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
960
1088
  schema_update: Optional[SchemaModelConfig] = None
961
1089
  schema_detail: Optional[SchemaModelConfig] = None
962
1090
  relations_serializers: dict[str, "Serializer"] = {}
1091
+ relations_as_id: list[str] = []
1092
+
1093
+ @classmethod
1094
+ def _get_relations_as_id(cls) -> list[str]:
1095
+ relations_as_id = cls._get_meta_data("relations_as_id")
1096
+ return relations_as_id or []
963
1097
 
964
1098
  @classmethod
965
1099
  def _get_meta_data(cls, attr_name: str) -> Any: