django-ninja-aio-crud 2.14.0__tar.gz → 2.15.1__tar.gz

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

Potentially problematic release.


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

Files changed (103) hide show
  1. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.github/workflows/docs.yml +2 -0
  2. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/model_serializer.md +58 -6
  4. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/serializers.md +42 -6
  5. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/__init__.py +1 -1
  6. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/auth.py +0 -5
  7. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/models/serializers.py +30 -13
  8. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/models.py +178 -0
  9. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_auth.py +2 -3
  10. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_serializers.py +440 -0
  11. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/views/test_viewset.py +1 -1
  12. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.github/dependabot.yml +0 -0
  13. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.github/workflows/coverage.yml +0 -0
  14. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.github/workflows/publish.yml +0 -0
  15. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.gitignore +0 -0
  16. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/.pre-commit-config.yaml +0 -0
  17. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/LICENSE +0 -0
  18. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/README.md +0 -0
  19. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/CNAME +0 -0
  20. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/authentication.md +0 -0
  21. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/models/model_util.md +0 -0
  22. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/pagination.md +0 -0
  23. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/renderers/orjson_renderer.md +0 -0
  24. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/api_view.md +0 -0
  25. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/api_view_set.md +0 -0
  26. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/decorators.md +0 -0
  27. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/api/views/mixins.md +0 -0
  28. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/auth.md +0 -0
  29. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/contributing.md +0 -0
  30. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/extra.css +0 -0
  31. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  32. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  33. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  34. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  35. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  36. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  37. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/installation.md +0 -0
  38. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/getting_started/quick_start.md +0 -0
  39. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/bar-swagger.png +0 -0
  40. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/favicon.ico +0 -0
  41. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/foo-swagger.png +0 -0
  42. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/logo.png +0 -0
  43. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  44. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/index.md +0 -0
  45. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/release_notes.md +0 -0
  46. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/requirements.txt +0 -0
  47. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/authentication.md +0 -0
  48. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/crud.md +0 -0
  49. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/filtering.md +0 -0
  50. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/docs/tutorial/model.md +0 -0
  51. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/main.py +0 -0
  52. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/mkdocs.yml +0 -0
  53. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/api.py +0 -0
  54. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/decorators/__init__.py +0 -0
  55. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/decorators/operations.py +0 -0
  56. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/decorators/views.py +0 -0
  57. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/exceptions.py +0 -0
  58. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/factory/__init__.py +0 -0
  59. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/factory/operations.py +0 -0
  60. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/helpers/__init__.py +0 -0
  61. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/helpers/api.py +0 -0
  62. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/helpers/query.py +0 -0
  63. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/models/__init__.py +0 -0
  64. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/models/utils.py +0 -0
  65. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/parsers.py +0 -0
  66. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/renders.py +0 -0
  67. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/__init__.py +0 -0
  68. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/api.py +0 -0
  69. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/filters.py +0 -0
  70. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/generics.py +0 -0
  71. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/schemas/helpers.py +0 -0
  72. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/types.py +0 -0
  73. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/views/__init__.py +0 -0
  74. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/views/api.py +0 -0
  75. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/ninja_aio/views/mixins.py +0 -0
  76. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/pyproject.toml +0 -0
  77. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/requirements.dev.txt +0 -0
  78. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/run-local-coverage.sh +0 -0
  79. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/__init__.py +0 -0
  80. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/__init__.py +0 -0
  81. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/test_decorators.py +0 -0
  82. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/test_exceptions_api.py +0 -0
  83. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/core/test_renderer_parser.py +0 -0
  84. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/__init__.py +0 -0
  85. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/literals.py +0 -0
  86. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/models.py +0 -0
  87. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/request.py +0 -0
  88. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/generics/views.py +0 -0
  89. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/helpers/__init__.py +0 -0
  90. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/helpers/test_many_to_many_api.py +0 -0
  91. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/models/__init__.py +0 -0
  92. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/models/test_model_util.py +0 -0
  93. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/models/test_models_extra.py +0 -0
  94. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/__init__.py +0 -0
  95. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/schema.py +0 -0
  96. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/serializers.py +0 -0
  97. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_app/views.py +0 -0
  98. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_decorators.py +0 -0
  99. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_exceptions.py +0 -0
  100. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_query_util.py +0 -0
  101. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/test_settings.py +0 -0
  102. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/views/__init__.py +0 -0
  103. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.1}/tests/views/test_views.py +0 -0
@@ -27,6 +27,7 @@ on:
27
27
  - "2.12"
28
28
  - "2.13"
29
29
  - "2.14"
30
+ - "2.15"
30
31
  make_latest:
31
32
  description: 'Set as "latest" and default?'
32
33
  type: boolean
@@ -57,6 +58,7 @@ on:
57
58
  - "2.12"
58
59
  - "2.13"
59
60
  - "2.14"
61
+ - "2.15"
60
62
  delete_confirm:
61
63
  description: 'Confirm deletion of the selected version'
62
64
  type: boolean
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.14.0
3
+ Version: 2.15.1
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -419,12 +419,14 @@ Use `relations_as_id` to serialize relation fields as IDs instead of nested obje
419
419
 
420
420
  | Relation Type | Output Type | Example Value |
421
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]` |
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.
428
430
 
429
431
  **Example:**
430
432
 
@@ -494,6 +496,56 @@ class Article(ModelSerializer):
494
496
  }
495
497
  ```
496
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
+
497
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:
498
550
 
499
551
  ```python
@@ -429,12 +429,14 @@ Use `relations_as_id` in Meta to serialize relation fields as IDs instead of nes
429
429
 
430
430
  | Relation Type | Output Type | Example Value |
431
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]` |
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.
438
440
 
439
441
  **Example:**
440
442
 
@@ -509,6 +511,40 @@ class ArticleSerializer(serializers.Serializer):
509
511
  }
510
512
  ```
511
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
+
512
548
  **Combining with relations_serializers:**
513
549
 
514
550
  You can use both `relations_as_id` and `relations_serializers` in the same serializer. Fields in `relations_as_id` take precedence:
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.14.0"
3
+ __version__ = "2.15.1"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -107,11 +107,6 @@ class AsyncJwtBearer(HttpBearer):
107
107
  """
108
108
  try:
109
109
  self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
110
- except ValueError:
111
- # raise AuthError(", ".join(exc.args), 401)
112
- return False
113
-
114
- try:
115
110
  self.validate_claims(self.dcd.claims)
116
111
  except errors.JoseError:
117
112
  return False
@@ -46,8 +46,23 @@ def _extract_pk(v: Any) -> Any:
46
46
  return v
47
47
 
48
48
 
49
- # Annotated type for extracting PK from model instances during serialization
50
- PkFromModel = Annotated[int, BeforeValidator(_extract_pk)]
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
51
66
 
52
67
 
53
68
  class BaseSerializer:
@@ -290,9 +305,8 @@ class BaseSerializer:
290
305
  """
291
306
  # Auto-resolve ModelSerializer with readable fields
292
307
  if isinstance(rel_model, ModelSerializerMeta):
293
- if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
294
- return rel_model.generate_related_s()
295
- return None
308
+ has_readable_fields = rel_model.get_fields("read") or rel_model.get_custom_fields("read")
309
+ return rel_model.generate_related_s() if has_readable_fields else None
296
310
 
297
311
  # Resolve from explicit serializer mapping
298
312
  rel_serializers = cls._get_relations_serializers() or {}
@@ -415,12 +429,14 @@ class BaseSerializer:
415
429
 
416
430
  # Handle relations_as_id for reverse relations
417
431
  if field_name in relations_as_id:
432
+ from ninja_aio.models.utils import ModelUtil
433
+ pk_field_type = ModelUtil(rel_model).pk_field_type
418
434
  if many:
419
435
  # For many relations, use PkFromModel to extract PKs from model instances
420
- return (field_name, list[PkFromModel], Field(default_factory=list))
436
+ return (field_name, list[PkFromModel[pk_field_type]], Field(default_factory=list))
421
437
  else:
422
438
  # For single reverse relations (ReverseOneToOne), extract pk
423
- return (field_name, PkFromModel | None, None)
439
+ return (field_name, PkFromModel[pk_field_type] | None, None)
424
440
 
425
441
  schema = cls._resolve_relation_schema(field_name, rel_model)
426
442
  if not schema:
@@ -455,15 +471,16 @@ class BaseSerializer:
455
471
 
456
472
  # Handle relations_as_id: serialize as the raw FK ID
457
473
  if field_name in relations_as_id:
474
+ from ninja_aio.models.utils import ModelUtil
475
+ pk_field_type = ModelUtil(rel_model).pk_field_type
458
476
  # Use PkFromModel to extract pk from the related instance during serialization
459
- return (field_name, PkFromModel | None, None)
477
+ return (field_name, PkFromModel[pk_field_type] | None, None)
460
478
 
461
479
  # Special case: ModelSerializer with no readable fields should be skipped entirely
462
- if isinstance(rel_model, ModelSerializerMeta):
463
- if not (
464
- rel_model.get_fields("read") or rel_model.get_custom_fields("read")
465
- ):
466
- return None
480
+ if isinstance(rel_model, ModelSerializerMeta) and not (
481
+ rel_model.get_fields("read") or rel_model.get_custom_fields("read")
482
+ ):
483
+ return None
467
484
 
468
485
  schema = cls._resolve_relation_schema(field_name, rel_model)
469
486
  if not schema:
@@ -1,3 +1,5 @@
1
+ import uuid
2
+
1
3
  from ninja_aio.models import ModelSerializer
2
4
  from django.db import models
3
5
 
@@ -298,3 +300,179 @@ class ArticleAsId(BaseTestModelSerializer):
298
300
  class ReadSerializer:
299
301
  fields = ["id", "name", "description", "tags_as_id"]
300
302
  relations_as_id = ["tags_as_id"]
303
+
304
+
305
+ # ==========================================================
306
+ # RELATIONS AS ID WITH UUID PK TEST MODELS
307
+ # ==========================================================
308
+
309
+
310
+ class BaseUUIDTestModel(ModelSerializer):
311
+ """Base model with UUID primary key."""
312
+
313
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
314
+ name = models.CharField(max_length=255)
315
+
316
+ class Meta:
317
+ abstract = True
318
+
319
+ class ReadSerializer:
320
+ fields = ["id", "name"]
321
+
322
+
323
+ class AuthorUUID(BaseUUIDTestModel):
324
+ """Author model with UUID PK for testing relations_as_id on reverse FK."""
325
+
326
+ class ReadSerializer:
327
+ fields = ["id", "name", "books_uuid"]
328
+ relations_as_id = ["books_uuid"]
329
+
330
+
331
+ class BookUUID(BaseUUIDTestModel):
332
+ """Book model with UUID PK for testing relations_as_id on forward FK."""
333
+
334
+ author_uuid = models.ForeignKey(
335
+ AuthorUUID,
336
+ on_delete=models.CASCADE,
337
+ related_name="books_uuid",
338
+ null=True,
339
+ blank=True,
340
+ )
341
+
342
+ class ReadSerializer:
343
+ fields = ["id", "name", "author_uuid"]
344
+ relations_as_id = ["author_uuid"]
345
+
346
+
347
+ class ProfileUUID(BaseUUIDTestModel):
348
+ """Profile model with UUID PK for testing relations_as_id on reverse O2O."""
349
+
350
+ class ReadSerializer:
351
+ fields = ["id", "name", "user_uuid"]
352
+ relations_as_id = ["user_uuid"]
353
+
354
+
355
+ class UserUUID(BaseUUIDTestModel):
356
+ """User model with UUID PK for testing relations_as_id on forward O2O."""
357
+
358
+ profile_uuid = models.OneToOneField(
359
+ ProfileUUID,
360
+ on_delete=models.CASCADE,
361
+ related_name="user_uuid",
362
+ null=True,
363
+ blank=True,
364
+ )
365
+
366
+ class ReadSerializer:
367
+ fields = ["id", "name", "profile_uuid"]
368
+ relations_as_id = ["profile_uuid"]
369
+
370
+
371
+ class TagUUID(BaseUUIDTestModel):
372
+ """Tag model with UUID PK for testing relations_as_id on reverse M2M."""
373
+
374
+ class ReadSerializer:
375
+ fields = ["id", "name", "articles_uuid"]
376
+ relations_as_id = ["articles_uuid"]
377
+
378
+
379
+ class ArticleUUID(BaseUUIDTestModel):
380
+ """Article model with UUID PK for testing relations_as_id on forward M2M."""
381
+
382
+ tags_uuid = models.ManyToManyField(
383
+ TagUUID,
384
+ related_name="articles_uuid",
385
+ blank=True,
386
+ )
387
+
388
+ class ReadSerializer:
389
+ fields = ["id", "name", "tags_uuid"]
390
+ relations_as_id = ["tags_uuid"]
391
+
392
+
393
+ # ==========================================================
394
+ # RELATIONS AS ID WITH STRING PK TEST MODELS
395
+ # ==========================================================
396
+
397
+
398
+ class BaseStringPKTestModel(ModelSerializer):
399
+ """Base model with string (CharField) primary key."""
400
+
401
+ id = models.CharField(primary_key=True, max_length=50)
402
+ name = models.CharField(max_length=255)
403
+
404
+ class Meta:
405
+ abstract = True
406
+
407
+ class ReadSerializer:
408
+ fields = ["id", "name"]
409
+
410
+
411
+ class AuthorStringPK(BaseStringPKTestModel):
412
+ """Author model with string PK for testing relations_as_id on reverse FK."""
413
+
414
+ class ReadSerializer:
415
+ fields = ["id", "name", "books_str"]
416
+ relations_as_id = ["books_str"]
417
+
418
+
419
+ class BookStringPK(BaseStringPKTestModel):
420
+ """Book model with string PK for testing relations_as_id on forward FK."""
421
+
422
+ author_str = models.ForeignKey(
423
+ AuthorStringPK,
424
+ on_delete=models.CASCADE,
425
+ related_name="books_str",
426
+ null=True,
427
+ blank=True,
428
+ )
429
+
430
+ class ReadSerializer:
431
+ fields = ["id", "name", "author_str"]
432
+ relations_as_id = ["author_str"]
433
+
434
+
435
+ class ProfileStringPK(BaseStringPKTestModel):
436
+ """Profile model with string PK for testing relations_as_id on reverse O2O."""
437
+
438
+ class ReadSerializer:
439
+ fields = ["id", "name", "user_str"]
440
+ relations_as_id = ["user_str"]
441
+
442
+
443
+ class UserStringPK(BaseStringPKTestModel):
444
+ """User model with string PK for testing relations_as_id on forward O2O."""
445
+
446
+ profile_str = models.OneToOneField(
447
+ ProfileStringPK,
448
+ on_delete=models.CASCADE,
449
+ related_name="user_str",
450
+ null=True,
451
+ blank=True,
452
+ )
453
+
454
+ class ReadSerializer:
455
+ fields = ["id", "name", "profile_str"]
456
+ relations_as_id = ["profile_str"]
457
+
458
+
459
+ class TagStringPK(BaseStringPKTestModel):
460
+ """Tag model with string PK for testing relations_as_id on reverse M2M."""
461
+
462
+ class ReadSerializer:
463
+ fields = ["id", "name", "articles_str"]
464
+ relations_as_id = ["articles_str"]
465
+
466
+
467
+ class ArticleStringPK(BaseStringPKTestModel):
468
+ """Article model with string PK for testing relations_as_id on forward M2M."""
469
+
470
+ tags_str = models.ManyToManyField(
471
+ TagStringPK,
472
+ related_name="articles_str",
473
+ blank=True,
474
+ )
475
+
476
+ class ReadSerializer:
477
+ fields = ["id", "name", "tags_str"]
478
+ relations_as_id = ["tags_str"]
@@ -138,12 +138,11 @@ class JwtAuthTests(TestCase):
138
138
  return "should-not-happen"
139
139
 
140
140
  bearer = TB()
141
- # The ValueError branch at 110-112 is for when jwt.decode raises ValueError
142
- # Mock jwt.decode to raise ValueError to test that branch
141
+
143
142
  import unittest.mock as mock
144
143
 
145
144
  with mock.patch("ninja_aio.auth.jwt.decode") as mock_decode:
146
- mock_decode.side_effect = ValueError("invalid token")
145
+ mock_decode.side_effect = errors.JoseError("invalid token")
147
146
  result = async_to_sync(bearer.authenticate)(HttpRequest(), "fake-token")
148
147
  self.assertFalse(result)
149
148