django-ninja-aio-crud 2.14.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 (103) hide show
  1. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/docs.yml +2 -0
  2. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/PKG-INFO +1 -1
  3. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/model_serializer.md +58 -6
  4. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/serializers.md +42 -6
  5. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/__init__.py +1 -1
  6. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/serializers.py +24 -5
  7. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/models.py +178 -0
  8. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_serializers.py +440 -0
  9. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/.github/dependabot.yml +0 -0
  10. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/coverage.yml +0 -0
  11. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/publish.yml +0 -0
  12. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/.gitignore +0 -0
  13. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/.pre-commit-config.yaml +0 -0
  14. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/LICENSE +0 -0
  15. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/README.md +0 -0
  16. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/CNAME +0 -0
  17. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/authentication.md +0 -0
  18. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/model_util.md +0 -0
  19. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/pagination.md +0 -0
  20. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/renderers/orjson_renderer.md +0 -0
  21. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/api_view.md +0 -0
  22. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/api_view_set.md +0 -0
  23. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/decorators.md +0 -0
  24. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/mixins.md +0 -0
  25. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/auth.md +0 -0
  26. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/contributing.md +0 -0
  27. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/extra.css +0 -0
  28. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  29. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  30. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  31. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  32. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  33. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  34. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/installation.md +0 -0
  35. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/quick_start.md +0 -0
  36. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/images/bar-swagger.png +0 -0
  37. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/images/favicon.ico +0 -0
  38. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/images/foo-swagger.png +0 -0
  39. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/images/logo.png +0 -0
  40. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  41. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/index.md +0 -0
  42. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/release_notes.md +0 -0
  43. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/requirements.txt +0 -0
  44. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/authentication.md +0 -0
  45. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/crud.md +0 -0
  46. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/filtering.md +0 -0
  47. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/model.md +0 -0
  48. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/main.py +0 -0
  49. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/mkdocs.yml +0 -0
  50. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/api.py +0 -0
  51. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/auth.py +0 -0
  52. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/__init__.py +0 -0
  53. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/operations.py +0 -0
  54. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/views.py +0 -0
  55. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/exceptions.py +0 -0
  56. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/factory/__init__.py +0 -0
  57. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/factory/operations.py +0 -0
  58. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/__init__.py +0 -0
  59. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/api.py +0 -0
  60. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/query.py +0 -0
  61. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/__init__.py +0 -0
  62. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/utils.py +0 -0
  63. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/parsers.py +0 -0
  64. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/renders.py +0 -0
  65. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/__init__.py +0 -0
  66. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/api.py +0 -0
  67. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/filters.py +0 -0
  68. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/generics.py +0 -0
  69. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/helpers.py +0 -0
  70. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/types.py +0 -0
  71. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/__init__.py +0 -0
  72. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/api.py +0 -0
  73. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/mixins.py +0 -0
  74. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/pyproject.toml +0 -0
  75. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/requirements.dev.txt +0 -0
  76. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/run-local-coverage.sh +0 -0
  77. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/__init__.py +0 -0
  78. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/core/__init__.py +0 -0
  79. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_decorators.py +0 -0
  80. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_exceptions_api.py +0 -0
  81. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_renderer_parser.py +0 -0
  82. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/generics/__init__.py +0 -0
  83. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/generics/literals.py +0 -0
  84. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/generics/models.py +0 -0
  85. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/generics/request.py +0 -0
  86. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/generics/views.py +0 -0
  87. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/helpers/__init__.py +0 -0
  88. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/helpers/test_many_to_many_api.py +0 -0
  89. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/models/__init__.py +0 -0
  90. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/models/test_model_util.py +0 -0
  91. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/models/test_models_extra.py +0 -0
  92. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/__init__.py +0 -0
  93. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/schema.py +0 -0
  94. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/serializers.py +0 -0
  95. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/views.py +0 -0
  96. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_auth.py +0 -0
  97. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_decorators.py +0 -0
  98. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_exceptions.py +0 -0
  99. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_query_util.py +0 -0
  100. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/test_settings.py +0 -0
  101. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/views/__init__.py +0 -0
  102. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/views/test_views.py +0 -0
  103. {django_ninja_aio_crud-2.14.0 → django_ninja_aio_crud-2.15.0}/tests/views/test_viewset.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.0
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.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -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:
@@ -415,12 +430,14 @@ class BaseSerializer:
415
430
 
416
431
  # Handle relations_as_id for reverse relations
417
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
418
435
  if many:
419
436
  # For many relations, use PkFromModel to extract PKs from model instances
420
- return (field_name, list[PkFromModel], Field(default_factory=list))
437
+ return (field_name, list[PkFromModel[pk_field_type]], Field(default_factory=list))
421
438
  else:
422
439
  # For single reverse relations (ReverseOneToOne), extract pk
423
- return (field_name, PkFromModel | None, None)
440
+ return (field_name, PkFromModel[pk_field_type] | None, None)
424
441
 
425
442
  schema = cls._resolve_relation_schema(field_name, rel_model)
426
443
  if not schema:
@@ -455,8 +472,10 @@ class BaseSerializer:
455
472
 
456
473
  # Handle relations_as_id: serialize as the raw FK ID
457
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
458
477
  # Use PkFromModel to extract pk from the related instance during serialization
459
- return (field_name, PkFromModel | None, None)
478
+ return (field_name, PkFromModel[pk_field_type] | None, None)
460
479
 
461
480
  # Special case: ModelSerializer with no readable fields should be skipped entirely
462
481
  if isinstance(rel_model, ModelSerializerMeta):
@@ -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"]
@@ -911,3 +911,443 @@ class RelationsAsIdIntegrationTestCase(TestCase):
911
911
  self.assertIsInstance(result.articles_as_id, list)
912
912
  self.assertEqual(len(result.articles_as_id), 1)
913
913
  self.assertIn(self.article.pk, result.articles_as_id)
914
+
915
+
916
+ @tag("serializers", "relations_as_id", "uuid_pk")
917
+ class RelationsAsIdUUIDModelSerializerTestCase(TestCase):
918
+ """Test cases for relations_as_id with UUID primary keys."""
919
+
920
+ def setUp(self):
921
+ warnings.simplefilter("ignore", UserWarning)
922
+
923
+ def test_forward_fk_uuid_relations_as_id_schema(self):
924
+ """Test forward FK field with UUID PK in relations_as_id generates UUID type."""
925
+ from tests.test_app.models import BookUUID
926
+ from uuid import UUID
927
+
928
+ schema = BookUUID.generate_read_s()
929
+ self.assertIsNotNone(schema)
930
+ self.assertIn("author_uuid", schema.model_fields)
931
+
932
+ field_info = schema.model_fields["author_uuid"]
933
+ self.assertTrue(
934
+ field_info.annotation is not None,
935
+ "author_uuid field should have a type annotation",
936
+ )
937
+
938
+ def test_reverse_fk_uuid_relations_as_id_schema(self):
939
+ """Test reverse FK field with UUID PK in relations_as_id generates list[UUID] type."""
940
+ from tests.test_app.models import AuthorUUID
941
+ from uuid import UUID
942
+
943
+ schema = AuthorUUID.generate_read_s()
944
+ self.assertIsNotNone(schema)
945
+ self.assertIn("books_uuid", schema.model_fields)
946
+
947
+ field_info = schema.model_fields["books_uuid"]
948
+ self.assertTrue(
949
+ field_info.annotation is not None,
950
+ "books_uuid field should have a type annotation",
951
+ )
952
+
953
+ def test_forward_o2o_uuid_relations_as_id_schema(self):
954
+ """Test forward O2O field with UUID PK in relations_as_id generates UUID type."""
955
+ from tests.test_app.models import UserUUID
956
+
957
+ schema = UserUUID.generate_read_s()
958
+ self.assertIsNotNone(schema)
959
+ self.assertIn("profile_uuid", schema.model_fields)
960
+
961
+ field_info = schema.model_fields["profile_uuid"]
962
+ self.assertTrue(
963
+ field_info.annotation is not None,
964
+ "profile_uuid field should have a type annotation",
965
+ )
966
+
967
+ def test_reverse_o2o_uuid_relations_as_id_schema(self):
968
+ """Test reverse O2O field with UUID PK in relations_as_id generates UUID type."""
969
+ from tests.test_app.models import ProfileUUID
970
+
971
+ schema = ProfileUUID.generate_read_s()
972
+ self.assertIsNotNone(schema)
973
+ self.assertIn("user_uuid", schema.model_fields)
974
+
975
+ field_info = schema.model_fields["user_uuid"]
976
+ self.assertTrue(
977
+ field_info.annotation is not None,
978
+ "user_uuid field should have a type annotation",
979
+ )
980
+
981
+ def test_forward_m2m_uuid_relations_as_id_schema(self):
982
+ """Test forward M2M field with UUID PK in relations_as_id generates list[UUID] type."""
983
+ from tests.test_app.models import ArticleUUID
984
+
985
+ schema = ArticleUUID.generate_read_s()
986
+ self.assertIsNotNone(schema)
987
+ self.assertIn("tags_uuid", schema.model_fields)
988
+
989
+ field_info = schema.model_fields["tags_uuid"]
990
+ self.assertTrue(
991
+ field_info.annotation is not None,
992
+ "tags_uuid field should have a type annotation",
993
+ )
994
+
995
+ def test_reverse_m2m_uuid_relations_as_id_schema(self):
996
+ """Test reverse M2M field with UUID PK in relations_as_id generates list[UUID] type."""
997
+ from tests.test_app.models import TagUUID
998
+
999
+ schema = TagUUID.generate_read_s()
1000
+ self.assertIsNotNone(schema)
1001
+ self.assertIn("articles_uuid", schema.model_fields)
1002
+
1003
+ field_info = schema.model_fields["articles_uuid"]
1004
+ self.assertTrue(
1005
+ field_info.annotation is not None,
1006
+ "articles_uuid field should have a type annotation",
1007
+ )
1008
+
1009
+
1010
+ @tag("serializers", "relations_as_id", "uuid_pk", "integration")
1011
+ class RelationsAsIdUUIDIntegrationTestCase(TestCase):
1012
+ """Integration tests for relations_as_id with UUID primary keys and actual data serialization."""
1013
+
1014
+ @classmethod
1015
+ def setUpTestData(cls):
1016
+ from tests.test_app.models import (
1017
+ AuthorUUID,
1018
+ BookUUID,
1019
+ ProfileUUID,
1020
+ UserUUID,
1021
+ TagUUID,
1022
+ ArticleUUID,
1023
+ )
1024
+
1025
+ # Create test data for FK relations with UUID PKs
1026
+ cls.author = AuthorUUID.objects.create(name="UUID Author 1")
1027
+ cls.book1 = BookUUID.objects.create(name="UUID Book 1", author_uuid=cls.author)
1028
+ cls.book2 = BookUUID.objects.create(name="UUID Book 2", author_uuid=cls.author)
1029
+ cls.book_no_author = BookUUID.objects.create(name="UUID Book 3", author_uuid=None)
1030
+
1031
+ # Create test data for O2O relations with UUID PKs
1032
+ cls.profile = ProfileUUID.objects.create(name="UUID Profile 1")
1033
+ cls.user = UserUUID.objects.create(name="UUID User 1", profile_uuid=cls.profile)
1034
+
1035
+ # Create test data for M2M relations with UUID PKs
1036
+ cls.tag1 = TagUUID.objects.create(name="UUID Tag 1")
1037
+ cls.tag2 = TagUUID.objects.create(name="UUID Tag 2")
1038
+ cls.article = ArticleUUID.objects.create(name="UUID Article 1")
1039
+ cls.article.tags_uuid.add(cls.tag1, cls.tag2)
1040
+
1041
+ def setUp(self):
1042
+ warnings.simplefilter("ignore", UserWarning)
1043
+
1044
+ def test_forward_fk_uuid_serialization(self):
1045
+ """Test forward FK field with UUID PK serializes as UUID."""
1046
+ from tests.test_app.models import BookUUID
1047
+ from uuid import UUID
1048
+
1049
+ schema = BookUUID.generate_read_s()
1050
+ result = schema.from_orm(self.book1)
1051
+
1052
+ self.assertIsInstance(result.author_uuid, UUID)
1053
+ self.assertEqual(result.author_uuid, self.author.pk)
1054
+
1055
+ def test_forward_fk_uuid_null_serialization(self):
1056
+ """Test forward FK field with UUID PK and null value serializes as None."""
1057
+ from tests.test_app.models import BookUUID
1058
+
1059
+ schema = BookUUID.generate_read_s()
1060
+ result = schema.from_orm(self.book_no_author)
1061
+
1062
+ self.assertIsNone(result.author_uuid)
1063
+
1064
+ def test_reverse_fk_uuid_serialization(self):
1065
+ """Test reverse FK field with UUID PK serializes as list of UUIDs."""
1066
+ from tests.test_app.models import AuthorUUID
1067
+ from uuid import UUID
1068
+
1069
+ author = AuthorUUID.objects.prefetch_related("books_uuid").get(pk=self.author.pk)
1070
+
1071
+ schema = AuthorUUID.generate_read_s()
1072
+ result = schema.from_orm(author)
1073
+
1074
+ self.assertIsInstance(result.books_uuid, list)
1075
+ self.assertEqual(len(result.books_uuid), 2)
1076
+ for book_id in result.books_uuid:
1077
+ self.assertIsInstance(book_id, UUID)
1078
+ self.assertIn(self.book1.pk, result.books_uuid)
1079
+ self.assertIn(self.book2.pk, result.books_uuid)
1080
+
1081
+ def test_forward_o2o_uuid_serialization(self):
1082
+ """Test forward O2O field with UUID PK serializes as UUID."""
1083
+ from tests.test_app.models import UserUUID
1084
+ from uuid import UUID
1085
+
1086
+ schema = UserUUID.generate_read_s()
1087
+ result = schema.from_orm(self.user)
1088
+
1089
+ self.assertIsInstance(result.profile_uuid, UUID)
1090
+ self.assertEqual(result.profile_uuid, self.profile.pk)
1091
+
1092
+ def test_reverse_o2o_uuid_serialization(self):
1093
+ """Test reverse O2O field with UUID PK serializes as UUID."""
1094
+ from tests.test_app.models import ProfileUUID
1095
+ from uuid import UUID
1096
+
1097
+ profile = ProfileUUID.objects.select_related("user_uuid").get(pk=self.profile.pk)
1098
+
1099
+ schema = ProfileUUID.generate_read_s()
1100
+ result = schema.from_orm(profile)
1101
+
1102
+ self.assertIsInstance(result.user_uuid, UUID)
1103
+ self.assertEqual(result.user_uuid, self.user.pk)
1104
+
1105
+ def test_forward_m2m_uuid_serialization(self):
1106
+ """Test forward M2M field with UUID PK serializes as list of UUIDs."""
1107
+ from tests.test_app.models import ArticleUUID
1108
+ from uuid import UUID
1109
+
1110
+ article = ArticleUUID.objects.prefetch_related("tags_uuid").get(pk=self.article.pk)
1111
+
1112
+ schema = ArticleUUID.generate_read_s()
1113
+ result = schema.from_orm(article)
1114
+
1115
+ self.assertIsInstance(result.tags_uuid, list)
1116
+ self.assertEqual(len(result.tags_uuid), 2)
1117
+ for tag_id in result.tags_uuid:
1118
+ self.assertIsInstance(tag_id, UUID)
1119
+ self.assertIn(self.tag1.pk, result.tags_uuid)
1120
+ self.assertIn(self.tag2.pk, result.tags_uuid)
1121
+
1122
+ def test_reverse_m2m_uuid_serialization(self):
1123
+ """Test reverse M2M field with UUID PK serializes as list of UUIDs."""
1124
+ from tests.test_app.models import TagUUID
1125
+ from uuid import UUID
1126
+
1127
+ tag = TagUUID.objects.prefetch_related("articles_uuid").get(pk=self.tag1.pk)
1128
+
1129
+ schema = TagUUID.generate_read_s()
1130
+ result = schema.from_orm(tag)
1131
+
1132
+ self.assertIsInstance(result.articles_uuid, list)
1133
+ self.assertEqual(len(result.articles_uuid), 1)
1134
+ self.assertIsInstance(result.articles_uuid[0], UUID)
1135
+ self.assertIn(self.article.pk, result.articles_uuid)
1136
+
1137
+
1138
+ @tag("serializers", "relations_as_id", "string_pk")
1139
+ class RelationsAsIdStringPKModelSerializerTestCase(TestCase):
1140
+ """Test cases for relations_as_id with string (CharField) primary keys."""
1141
+
1142
+ def setUp(self):
1143
+ warnings.simplefilter("ignore", UserWarning)
1144
+
1145
+ def test_forward_fk_string_relations_as_id_schema(self):
1146
+ """Test forward FK field with string PK in relations_as_id generates str type."""
1147
+ from tests.test_app.models import BookStringPK
1148
+
1149
+ schema = BookStringPK.generate_read_s()
1150
+ self.assertIsNotNone(schema)
1151
+ self.assertIn("author_str", schema.model_fields)
1152
+
1153
+ field_info = schema.model_fields["author_str"]
1154
+ self.assertTrue(
1155
+ field_info.annotation is not None,
1156
+ "author_str field should have a type annotation",
1157
+ )
1158
+
1159
+ def test_reverse_fk_string_relations_as_id_schema(self):
1160
+ """Test reverse FK field with string PK in relations_as_id generates list[str] type."""
1161
+ from tests.test_app.models import AuthorStringPK
1162
+
1163
+ schema = AuthorStringPK.generate_read_s()
1164
+ self.assertIsNotNone(schema)
1165
+ self.assertIn("books_str", schema.model_fields)
1166
+
1167
+ field_info = schema.model_fields["books_str"]
1168
+ self.assertTrue(
1169
+ field_info.annotation is not None,
1170
+ "books_str field should have a type annotation",
1171
+ )
1172
+
1173
+ def test_forward_o2o_string_relations_as_id_schema(self):
1174
+ """Test forward O2O field with string PK in relations_as_id generates str type."""
1175
+ from tests.test_app.models import UserStringPK
1176
+
1177
+ schema = UserStringPK.generate_read_s()
1178
+ self.assertIsNotNone(schema)
1179
+ self.assertIn("profile_str", schema.model_fields)
1180
+
1181
+ field_info = schema.model_fields["profile_str"]
1182
+ self.assertTrue(
1183
+ field_info.annotation is not None,
1184
+ "profile_str field should have a type annotation",
1185
+ )
1186
+
1187
+ def test_reverse_o2o_string_relations_as_id_schema(self):
1188
+ """Test reverse O2O field with string PK in relations_as_id generates str type."""
1189
+ from tests.test_app.models import ProfileStringPK
1190
+
1191
+ schema = ProfileStringPK.generate_read_s()
1192
+ self.assertIsNotNone(schema)
1193
+ self.assertIn("user_str", schema.model_fields)
1194
+
1195
+ field_info = schema.model_fields["user_str"]
1196
+ self.assertTrue(
1197
+ field_info.annotation is not None,
1198
+ "user_str field should have a type annotation",
1199
+ )
1200
+
1201
+ def test_forward_m2m_string_relations_as_id_schema(self):
1202
+ """Test forward M2M field with string PK in relations_as_id generates list[str] type."""
1203
+ from tests.test_app.models import ArticleStringPK
1204
+
1205
+ schema = ArticleStringPK.generate_read_s()
1206
+ self.assertIsNotNone(schema)
1207
+ self.assertIn("tags_str", schema.model_fields)
1208
+
1209
+ field_info = schema.model_fields["tags_str"]
1210
+ self.assertTrue(
1211
+ field_info.annotation is not None,
1212
+ "tags_str field should have a type annotation",
1213
+ )
1214
+
1215
+ def test_reverse_m2m_string_relations_as_id_schema(self):
1216
+ """Test reverse M2M field with string PK in relations_as_id generates list[str] type."""
1217
+ from tests.test_app.models import TagStringPK
1218
+
1219
+ schema = TagStringPK.generate_read_s()
1220
+ self.assertIsNotNone(schema)
1221
+ self.assertIn("articles_str", schema.model_fields)
1222
+
1223
+ field_info = schema.model_fields["articles_str"]
1224
+ self.assertTrue(
1225
+ field_info.annotation is not None,
1226
+ "articles_str field should have a type annotation",
1227
+ )
1228
+
1229
+
1230
+ @tag("serializers", "relations_as_id", "string_pk", "integration")
1231
+ class RelationsAsIdStringPKIntegrationTestCase(TestCase):
1232
+ """Integration tests for relations_as_id with string primary keys and actual data serialization."""
1233
+
1234
+ @classmethod
1235
+ def setUpTestData(cls):
1236
+ from tests.test_app.models import (
1237
+ AuthorStringPK,
1238
+ BookStringPK,
1239
+ ProfileStringPK,
1240
+ UserStringPK,
1241
+ TagStringPK,
1242
+ ArticleStringPK,
1243
+ )
1244
+
1245
+ # Create test data for FK relations with string PKs
1246
+ cls.author = AuthorStringPK.objects.create(id="author-001", name="String Author 1")
1247
+ cls.book1 = BookStringPK.objects.create(id="book-001", name="String Book 1", author_str=cls.author)
1248
+ cls.book2 = BookStringPK.objects.create(id="book-002", name="String Book 2", author_str=cls.author)
1249
+ cls.book_no_author = BookStringPK.objects.create(id="book-003", name="String Book 3", author_str=None)
1250
+
1251
+ # Create test data for O2O relations with string PKs
1252
+ cls.profile = ProfileStringPK.objects.create(id="profile-001", name="String Profile 1")
1253
+ cls.user = UserStringPK.objects.create(id="user-001", name="String User 1", profile_str=cls.profile)
1254
+
1255
+ # Create test data for M2M relations with string PKs
1256
+ cls.tag1 = TagStringPK.objects.create(id="tag-001", name="String Tag 1")
1257
+ cls.tag2 = TagStringPK.objects.create(id="tag-002", name="String Tag 2")
1258
+ cls.article = ArticleStringPK.objects.create(id="article-001", name="String Article 1")
1259
+ cls.article.tags_str.add(cls.tag1, cls.tag2)
1260
+
1261
+ def setUp(self):
1262
+ warnings.simplefilter("ignore", UserWarning)
1263
+
1264
+ def test_forward_fk_string_serialization(self):
1265
+ """Test forward FK field with string PK serializes as str."""
1266
+ from tests.test_app.models import BookStringPK
1267
+
1268
+ schema = BookStringPK.generate_read_s()
1269
+ result = schema.from_orm(self.book1)
1270
+
1271
+ self.assertIsInstance(result.author_str, str)
1272
+ self.assertEqual(result.author_str, self.author.pk)
1273
+ self.assertEqual(result.author_str, "author-001")
1274
+
1275
+ def test_forward_fk_string_null_serialization(self):
1276
+ """Test forward FK field with string PK and null value serializes as None."""
1277
+ from tests.test_app.models import BookStringPK
1278
+
1279
+ schema = BookStringPK.generate_read_s()
1280
+ result = schema.from_orm(self.book_no_author)
1281
+
1282
+ self.assertIsNone(result.author_str)
1283
+
1284
+ def test_reverse_fk_string_serialization(self):
1285
+ """Test reverse FK field with string PK serializes as list of strs."""
1286
+ from tests.test_app.models import AuthorStringPK
1287
+
1288
+ author = AuthorStringPK.objects.prefetch_related("books_str").get(pk=self.author.pk)
1289
+
1290
+ schema = AuthorStringPK.generate_read_s()
1291
+ result = schema.from_orm(author)
1292
+
1293
+ self.assertIsInstance(result.books_str, list)
1294
+ self.assertEqual(len(result.books_str), 2)
1295
+ for book_id in result.books_str:
1296
+ self.assertIsInstance(book_id, str)
1297
+ self.assertIn(self.book1.pk, result.books_str)
1298
+ self.assertIn(self.book2.pk, result.books_str)
1299
+
1300
+ def test_forward_o2o_string_serialization(self):
1301
+ """Test forward O2O field with string PK serializes as str."""
1302
+ from tests.test_app.models import UserStringPK
1303
+
1304
+ schema = UserStringPK.generate_read_s()
1305
+ result = schema.from_orm(self.user)
1306
+
1307
+ self.assertIsInstance(result.profile_str, str)
1308
+ self.assertEqual(result.profile_str, self.profile.pk)
1309
+ self.assertEqual(result.profile_str, "profile-001")
1310
+
1311
+ def test_reverse_o2o_string_serialization(self):
1312
+ """Test reverse O2O field with string PK serializes as str."""
1313
+ from tests.test_app.models import ProfileStringPK
1314
+
1315
+ profile = ProfileStringPK.objects.select_related("user_str").get(pk=self.profile.pk)
1316
+
1317
+ schema = ProfileStringPK.generate_read_s()
1318
+ result = schema.from_orm(profile)
1319
+
1320
+ self.assertIsInstance(result.user_str, str)
1321
+ self.assertEqual(result.user_str, self.user.pk)
1322
+ self.assertEqual(result.user_str, "user-001")
1323
+
1324
+ def test_forward_m2m_string_serialization(self):
1325
+ """Test forward M2M field with string PK serializes as list of strs."""
1326
+ from tests.test_app.models import ArticleStringPK
1327
+
1328
+ article = ArticleStringPK.objects.prefetch_related("tags_str").get(pk=self.article.pk)
1329
+
1330
+ schema = ArticleStringPK.generate_read_s()
1331
+ result = schema.from_orm(article)
1332
+
1333
+ self.assertIsInstance(result.tags_str, list)
1334
+ self.assertEqual(len(result.tags_str), 2)
1335
+ for tag_id in result.tags_str:
1336
+ self.assertIsInstance(tag_id, str)
1337
+ self.assertIn(self.tag1.pk, result.tags_str)
1338
+ self.assertIn(self.tag2.pk, result.tags_str)
1339
+
1340
+ def test_reverse_m2m_string_serialization(self):
1341
+ """Test reverse M2M field with string PK serializes as list of strs."""
1342
+ from tests.test_app.models import TagStringPK
1343
+
1344
+ tag = TagStringPK.objects.prefetch_related("articles_str").get(pk=self.tag1.pk)
1345
+
1346
+ schema = TagStringPK.generate_read_s()
1347
+ result = schema.from_orm(tag)
1348
+
1349
+ self.assertIsInstance(result.articles_str, list)
1350
+ self.assertEqual(len(result.articles_str), 1)
1351
+ self.assertIsInstance(result.articles_str[0], str)
1352
+ self.assertIn(self.article.pk, result.articles_str)
1353
+ self.assertEqual(result.articles_str[0], "article-001")