django-ninja-aio-crud 2.6.1__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ninja-aio-crud might be problematic. Click here for more details.
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/workflows/docs.yml +2 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/models/serializers.md +179 -3
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/models/serializers.py +146 -27
- django_ninja_aio_crud-2.7.0/tests/test_serializers.py +291 -0
- django_ninja_aio_crud-2.6.1/tests/test_serializers.py +0 -106
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/README.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/api_view_set.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/main.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/helpers/api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/views/api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/models.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/serializers.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/views/test_views.py +0 -0
- {django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/views/test_viewset.py +0 -0
|
@@ -18,6 +18,7 @@ on:
|
|
|
18
18
|
- "2.4"
|
|
19
19
|
- "2.5"
|
|
20
20
|
- "2.6"
|
|
21
|
+
- "2.6.1"
|
|
21
22
|
make_latest:
|
|
22
23
|
description: 'Set as "latest" and default?'
|
|
23
24
|
type: boolean
|
|
@@ -39,6 +40,7 @@ on:
|
|
|
39
40
|
- "2.4"
|
|
40
41
|
- "2.5"
|
|
41
42
|
- "2.6"
|
|
43
|
+
- "2.6.1"
|
|
42
44
|
delete_confirm:
|
|
43
45
|
description: 'Confirm deletion of the selected version'
|
|
44
46
|
type: boolean
|
|
@@ -20,7 +20,7 @@ While both `ModelSerializer` and `Serializer` provide schema generation and CRUD
|
|
|
20
20
|
| Schema generation | On-demand via generate_*() methods | On-demand via generate_*() methods |
|
|
21
21
|
| Usage | Inherit from ModelSerializer | Separate serializer class |
|
|
22
22
|
| Query optimization | QuerySet nested class | QuerySet nested class (inherited) |
|
|
23
|
-
| Relation serializers | Auto-resolved | Explicit via relations_serializers (supports string refs) |
|
|
23
|
+
| Relation serializers | Auto-resolved | Explicit via relations_serializers (supports string refs & Union) |
|
|
24
24
|
|
|
25
25
|
## Key points
|
|
26
26
|
|
|
@@ -28,6 +28,7 @@ While both `ModelSerializer` and `Serializer` provide schema generation and CRUD
|
|
|
28
28
|
- Generates read/create/update/related schemas on demand via ninja.orm.create_schema.
|
|
29
29
|
- Supports explicit relation serializers for forward and reverse relations.
|
|
30
30
|
- **Supports string references in `relations_serializers` for forward/circular dependencies**.
|
|
31
|
+
- **Supports Union types for polymorphic relations** (e.g., generic foreign keys, content types).
|
|
31
32
|
- Plays nicely with APIViewSet to auto-wire schemas and queryset handling.
|
|
32
33
|
|
|
33
34
|
## Configuration
|
|
@@ -38,7 +39,7 @@ Define a Serializer subclass with a nested Meta:
|
|
|
38
39
|
- **schema_in**: SchemaModelConfig for create inputs
|
|
39
40
|
- **schema_out**: SchemaModelConfig for read outputs
|
|
40
41
|
- **schema_update**: SchemaModelConfig for patch/update inputs
|
|
41
|
-
- **relations_serializers**: Mapping of relation field name -> Serializer class **
|
|
42
|
+
- **relations_serializers**: Mapping of relation field name -> Serializer class, **string reference**, or **Union of serializers** (supports forward/circular dependencies and polymorphic relations)
|
|
42
43
|
|
|
43
44
|
SchemaModelConfig fields:
|
|
44
45
|
|
|
@@ -180,16 +181,191 @@ class ArticleSerializer(serializers.Serializer):
|
|
|
180
181
|
}
|
|
181
182
|
```
|
|
182
183
|
|
|
184
|
+
**String Reference Formats:**
|
|
185
|
+
|
|
186
|
+
1. **Class name in the same module:**
|
|
187
|
+
```python
|
|
188
|
+
relations_serializers = {
|
|
189
|
+
"articles": "ArticleSerializer", # Resolved in current module
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
2. **Absolute import path:**
|
|
194
|
+
```python
|
|
195
|
+
relations_serializers = {
|
|
196
|
+
"articles": "myapp.serializers.ArticleSerializer", # Full import path
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
183
200
|
**String Reference Requirements:**
|
|
184
|
-
- String
|
|
201
|
+
- String can be the class name of a serializer in the same module, or an absolute import path
|
|
202
|
+
- Absolute paths use dot notation: `"package.module.ClassName"`
|
|
185
203
|
- References are resolved lazily when schemas are generated
|
|
186
204
|
- Both forward and circular references are supported
|
|
187
205
|
|
|
206
|
+
**Example: Cross-Module References with Absolute Paths**
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# myapp/serializers.py
|
|
210
|
+
from ninja_aio.models import serializers
|
|
211
|
+
from . import models
|
|
212
|
+
|
|
213
|
+
class ArticleSerializer(serializers.Serializer):
|
|
214
|
+
class Meta:
|
|
215
|
+
model = models.Article
|
|
216
|
+
schema_out = serializers.SchemaModelConfig(
|
|
217
|
+
fields=["id", "title", "author"]
|
|
218
|
+
)
|
|
219
|
+
relations_serializers = {
|
|
220
|
+
# Reference a serializer from another module
|
|
221
|
+
"author": "users.serializers.UserSerializer",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# users/serializers.py
|
|
225
|
+
from ninja_aio.models import serializers
|
|
226
|
+
from . import models
|
|
227
|
+
|
|
228
|
+
class UserSerializer(serializers.Serializer):
|
|
229
|
+
class Meta:
|
|
230
|
+
model = models.User
|
|
231
|
+
schema_out = serializers.SchemaModelConfig(
|
|
232
|
+
fields=["id", "username", "email", "articles"]
|
|
233
|
+
)
|
|
234
|
+
relations_serializers = {
|
|
235
|
+
# Reference back to the article serializer
|
|
236
|
+
"articles": "myapp.serializers.ArticleSerializer",
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Union Types for Polymorphic Relations
|
|
241
|
+
|
|
242
|
+
You can use `Union` types in `relations_serializers` to handle polymorphic relationships where a field can reference multiple possible serializer types. This is particularly useful for generic foreign keys, content types, or any scenario where a relation can point to different model types.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from typing import Union
|
|
246
|
+
from ninja_aio.models import serializers
|
|
247
|
+
from . import models
|
|
248
|
+
|
|
249
|
+
class VideoSerializer(serializers.Serializer):
|
|
250
|
+
class Meta:
|
|
251
|
+
model = models.Video
|
|
252
|
+
schema_out = serializers.SchemaModelConfig(
|
|
253
|
+
fields=["id", "title", "duration", "url"]
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
class ImageSerializer(serializers.Serializer):
|
|
257
|
+
class Meta:
|
|
258
|
+
model = models.Image
|
|
259
|
+
schema_out = serializers.SchemaModelConfig(
|
|
260
|
+
fields=["id", "title", "width", "height", "url"]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
class CommentSerializer(serializers.Serializer):
|
|
264
|
+
class Meta:
|
|
265
|
+
model = models.Comment
|
|
266
|
+
schema_out = serializers.SchemaModelConfig(
|
|
267
|
+
fields=["id", "text", "content_object"]
|
|
268
|
+
)
|
|
269
|
+
relations_serializers = {
|
|
270
|
+
# content_object can be a Video or Image
|
|
271
|
+
"content_object": Union[VideoSerializer, ImageSerializer],
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Union Type Formats:**
|
|
276
|
+
|
|
277
|
+
1. **Direct class references:**
|
|
278
|
+
```python
|
|
279
|
+
relations_serializers = {
|
|
280
|
+
"field": Union[SerializerA, SerializerB],
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
2. **String references:**
|
|
285
|
+
```python
|
|
286
|
+
relations_serializers = {
|
|
287
|
+
"field": Union["SerializerA", "SerializerB"],
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
3. **Mixed class and string references:**
|
|
292
|
+
```python
|
|
293
|
+
relations_serializers = {
|
|
294
|
+
"field": Union[SerializerA, "SerializerB"],
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
4. **Absolute import paths:**
|
|
299
|
+
```python
|
|
300
|
+
relations_serializers = {
|
|
301
|
+
"field": Union["myapp.serializers.SerializerA", SerializerB],
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Use Cases for Union Types:**
|
|
306
|
+
|
|
307
|
+
- **Polymorphic relations:** Generic foreign keys or Django ContentType relations
|
|
308
|
+
- **Flexible APIs:** Different response formats for the same field based on runtime type
|
|
309
|
+
- **Gradual migrations:** Transitioning between different serializer implementations
|
|
310
|
+
- **Multi-tenant systems:** Different serialization requirements per tenant
|
|
311
|
+
|
|
312
|
+
**Complete Polymorphic Example:**
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from typing import Union
|
|
316
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
317
|
+
from ninja_aio.models import serializers
|
|
318
|
+
|
|
319
|
+
# Models
|
|
320
|
+
class Comment(models.Model):
|
|
321
|
+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
322
|
+
object_id = models.PositiveIntegerField()
|
|
323
|
+
content_object = GenericForeignKey('content_type', 'object_id')
|
|
324
|
+
text = models.TextField()
|
|
325
|
+
|
|
326
|
+
# Serializers for different content types
|
|
327
|
+
class BlogPostSerializer(serializers.Serializer):
|
|
328
|
+
class Meta:
|
|
329
|
+
model = models.BlogPost
|
|
330
|
+
schema_out = serializers.SchemaModelConfig(
|
|
331
|
+
fields=["id", "title", "body", "published_at"]
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
class ProductSerializer(serializers.Serializer):
|
|
335
|
+
class Meta:
|
|
336
|
+
model = models.Product
|
|
337
|
+
schema_out = serializers.SchemaModelConfig(
|
|
338
|
+
fields=["id", "name", "price", "stock"]
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
class EventSerializer(serializers.Serializer):
|
|
342
|
+
class Meta:
|
|
343
|
+
model = models.Event
|
|
344
|
+
schema_out = serializers.SchemaModelConfig(
|
|
345
|
+
fields=["id", "name", "date", "location"]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Comment serializer with Union support
|
|
349
|
+
class CommentSerializer(serializers.Serializer):
|
|
350
|
+
class Meta:
|
|
351
|
+
model = Comment
|
|
352
|
+
schema_out = serializers.SchemaModelConfig(
|
|
353
|
+
fields=["id", "text", "created_at", "content_object"]
|
|
354
|
+
)
|
|
355
|
+
relations_serializers = {
|
|
356
|
+
# Comments can be on blog posts, products, or events
|
|
357
|
+
"content_object": Union[BlogPostSerializer, ProductSerializer, EventSerializer],
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
188
361
|
Notes:
|
|
189
362
|
|
|
190
363
|
- Forward relations are included as plain fields unless a related ModelSerializer/Serializer is declared.
|
|
191
364
|
- Reverse relations require an entry in relations_serializers when using vanilla Django models.
|
|
192
365
|
- When the related model is a ModelSerializer, related schemas can be auto-resolved.
|
|
366
|
+
- Absolute import paths are useful for cross-module references and avoiding circular import issues at module load time.
|
|
367
|
+
- Union types are resolved lazily, so forward and circular references work seamlessly.
|
|
368
|
+
- The schema generator will create a union of all possible schemas from the serializers in the Union.
|
|
193
369
|
|
|
194
370
|
## Using with APIViewSet
|
|
195
371
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, List, Optional
|
|
1
|
+
from typing import Any, List, Optional, Union, get_args, get_origin, ForwardRef
|
|
2
2
|
import warnings
|
|
3
3
|
import sys
|
|
4
4
|
|
|
@@ -69,19 +69,14 @@ class BaseSerializer:
|
|
|
69
69
|
raise NotImplementedError
|
|
70
70
|
|
|
71
71
|
@classmethod
|
|
72
|
-
def
|
|
72
|
+
def _resolve_string_reference(cls, string_ref: str) -> type:
|
|
73
73
|
"""
|
|
74
|
-
Resolve a serializer reference
|
|
75
|
-
|
|
76
|
-
This method performs lazy resolution, meaning it will attempt to resolve
|
|
77
|
-
string references only when called, allowing for forward references and
|
|
78
|
-
circular dependencies between serializers in the same module.
|
|
74
|
+
Resolve a string serializer reference to an actual class.
|
|
79
75
|
|
|
80
76
|
Parameters
|
|
81
77
|
----------
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
or an actual serializer class.
|
|
78
|
+
string_ref : str
|
|
79
|
+
String reference (local class name or absolute import path).
|
|
85
80
|
|
|
86
81
|
Returns
|
|
87
82
|
-------
|
|
@@ -93,35 +88,152 @@ class BaseSerializer:
|
|
|
93
88
|
ValueError
|
|
94
89
|
If the string reference cannot be resolved.
|
|
95
90
|
"""
|
|
96
|
-
#
|
|
97
|
-
if
|
|
98
|
-
|
|
91
|
+
# Check if it's an absolute import path (contains dots)
|
|
92
|
+
if "." in string_ref:
|
|
93
|
+
# Absolute import path: "myapp.serializers.UserSerializer"
|
|
94
|
+
module_path, class_name = string_ref.rsplit(".", 1)
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
try:
|
|
97
|
+
# Try to get or import the module
|
|
98
|
+
module = sys.modules.get(module_path)
|
|
99
|
+
if module is None:
|
|
100
|
+
import importlib
|
|
101
|
+
module = importlib.import_module(module_path)
|
|
102
|
+
|
|
103
|
+
# Get the serializer class from the module
|
|
104
|
+
serializer_class = getattr(module, class_name, None)
|
|
105
|
+
|
|
106
|
+
if serializer_class is None:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Cannot resolve serializer reference '{string_ref}': "
|
|
109
|
+
f"class '{class_name}' not found in module '{module_path}'."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return serializer_class
|
|
113
|
+
except ImportError as e:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Cannot resolve serializer reference '{string_ref}': "
|
|
116
|
+
f"failed to import module '{module_path}': {e}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Local reference: simple class name in the same module
|
|
101
120
|
module = sys.modules.get(cls.__module__)
|
|
102
121
|
|
|
103
122
|
if module is None:
|
|
104
123
|
raise ValueError(
|
|
105
|
-
f"Cannot resolve serializer reference '{
|
|
124
|
+
f"Cannot resolve serializer reference '{string_ref}': "
|
|
106
125
|
f"module '{cls.__module__}' not found in sys.modules."
|
|
107
126
|
)
|
|
108
127
|
|
|
109
|
-
|
|
110
|
-
serializer_class = getattr(module, serializer_ref, None)
|
|
128
|
+
serializer_class = getattr(module, string_ref, None)
|
|
111
129
|
|
|
112
130
|
if serializer_class is None:
|
|
113
131
|
raise ValueError(
|
|
114
|
-
f"Cannot resolve serializer reference '{
|
|
115
|
-
f"Make sure the serializer class '{
|
|
132
|
+
f"Cannot resolve serializer reference '{string_ref}' in module '{cls.__module__}'. "
|
|
133
|
+
f"Make sure the serializer class '{string_ref}' is defined in the same module as {cls.__name__}."
|
|
116
134
|
)
|
|
117
135
|
|
|
118
136
|
return serializer_class
|
|
119
137
|
|
|
138
|
+
@classmethod
|
|
139
|
+
def _resolve_serializer_reference(cls, serializer_ref: str | type | Any) -> type | Any:
|
|
140
|
+
"""
|
|
141
|
+
Resolve a serializer reference that may be a string, a class, or a Union of serializers.
|
|
142
|
+
|
|
143
|
+
This method performs lazy resolution, meaning it will attempt to resolve
|
|
144
|
+
string references only when called, allowing for forward references and
|
|
145
|
+
circular dependencies between serializers in the same module.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
serializer_ref : str | type | Union
|
|
150
|
+
Either a string reference to a serializer class, an actual serializer class,
|
|
151
|
+
or a Union of serializer references. String references can be:
|
|
152
|
+
- A class name in the same module (e.g., "UserSerializer")
|
|
153
|
+
- An absolute import path (e.g., "myapp.serializers.UserSerializer")
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
type | Union
|
|
158
|
+
The resolved serializer class or Union of serializer classes.
|
|
159
|
+
|
|
160
|
+
Raises
|
|
161
|
+
------
|
|
162
|
+
ValueError
|
|
163
|
+
If the string reference cannot be resolved.
|
|
164
|
+
|
|
165
|
+
Examples
|
|
166
|
+
--------
|
|
167
|
+
>>> # Single reference
|
|
168
|
+
>>> cls._resolve_serializer_reference("UserSerializer")
|
|
169
|
+
>>> cls._resolve_serializer_reference(UserSerializer)
|
|
170
|
+
>>>
|
|
171
|
+
>>> # Union reference
|
|
172
|
+
>>> from typing import Union
|
|
173
|
+
>>> cls._resolve_serializer_reference(Union[UserSerializer, AdminSerializer])
|
|
174
|
+
>>> cls._resolve_serializer_reference(Union["UserSerializer", "AdminSerializer"])
|
|
175
|
+
"""
|
|
176
|
+
# Handle Union types
|
|
177
|
+
origin = get_origin(serializer_ref)
|
|
178
|
+
if origin is Union:
|
|
179
|
+
resolved_types = tuple(
|
|
180
|
+
cls._resolve_serializer_reference(arg)
|
|
181
|
+
for arg in get_args(serializer_ref)
|
|
182
|
+
)
|
|
183
|
+
# Optimize single-type unions
|
|
184
|
+
if len(resolved_types) == 1:
|
|
185
|
+
return resolved_types[0]
|
|
186
|
+
# Create Union using indexing syntax for Python 3.10+ compatibility
|
|
187
|
+
return Union[resolved_types]
|
|
188
|
+
|
|
189
|
+
# Handle ForwardRef (created when using Union["StringType"])
|
|
190
|
+
if isinstance(serializer_ref, ForwardRef):
|
|
191
|
+
return cls._resolve_serializer_reference(serializer_ref.__forward_arg__)
|
|
192
|
+
|
|
193
|
+
# Handle string references
|
|
194
|
+
if isinstance(serializer_ref, str):
|
|
195
|
+
return cls._resolve_string_reference(serializer_ref)
|
|
196
|
+
|
|
197
|
+
# Already a class, return as-is
|
|
198
|
+
return serializer_ref
|
|
199
|
+
|
|
120
200
|
@classmethod
|
|
121
201
|
def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
|
|
122
202
|
# Optional in subclasses. Default to no explicit relation serializers.
|
|
123
203
|
return {}
|
|
124
204
|
|
|
205
|
+
@classmethod
|
|
206
|
+
def _generate_union_schema(cls, resolved_union: Any) -> Any:
|
|
207
|
+
"""
|
|
208
|
+
Generate a Union schema from multiple resolved serializers.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
resolved_union : Union
|
|
213
|
+
A Union type containing resolved serializer classes.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
Schema | Union[Schema, ...] | None
|
|
218
|
+
Union of generated schemas or None if all schemas are None.
|
|
219
|
+
"""
|
|
220
|
+
# Generate schemas for each serializer in the Union
|
|
221
|
+
schemas = tuple(
|
|
222
|
+
serializer_type.generate_related_s()
|
|
223
|
+
for serializer_type in get_args(resolved_union)
|
|
224
|
+
if serializer_type.generate_related_s() is not None
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if not schemas:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# Optimize single-schema unions
|
|
231
|
+
if len(schemas) == 1:
|
|
232
|
+
return schemas[0]
|
|
233
|
+
|
|
234
|
+
# Create Union of schemas using indexing syntax for Python 3.10+ compatibility
|
|
235
|
+
return Union[schemas]
|
|
236
|
+
|
|
125
237
|
@classmethod
|
|
126
238
|
def _resolve_relation_schema(cls, field_name: str, rel_model: models.Model):
|
|
127
239
|
"""
|
|
@@ -136,23 +248,30 @@ class BaseSerializer:
|
|
|
136
248
|
|
|
137
249
|
Returns
|
|
138
250
|
-------
|
|
139
|
-
Schema | None
|
|
140
|
-
Generated schema or None if cannot be resolved.
|
|
251
|
+
Schema | Union[Schema, ...] | None
|
|
252
|
+
Generated schema, Union of schemas, or None if cannot be resolved.
|
|
141
253
|
"""
|
|
142
|
-
#
|
|
254
|
+
# Auto-resolve ModelSerializer with readable fields
|
|
143
255
|
if isinstance(rel_model, ModelSerializerMeta):
|
|
144
256
|
if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
|
|
145
257
|
return rel_model.generate_related_s()
|
|
146
258
|
return None
|
|
147
259
|
|
|
148
|
-
#
|
|
260
|
+
# Resolve from explicit serializer mapping
|
|
149
261
|
rel_serializers = cls._get_relations_serializers() or {}
|
|
150
262
|
serializer_ref = rel_serializers.get(field_name)
|
|
151
|
-
if serializer_ref:
|
|
152
|
-
serializer = cls._resolve_serializer_reference(serializer_ref)
|
|
153
|
-
return serializer.generate_related_s()
|
|
154
263
|
|
|
155
|
-
|
|
264
|
+
if not serializer_ref:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
resolved = cls._resolve_serializer_reference(serializer_ref)
|
|
268
|
+
|
|
269
|
+
# Handle Union of serializers
|
|
270
|
+
if get_origin(resolved) is Union:
|
|
271
|
+
return cls._generate_union_schema(resolved)
|
|
272
|
+
|
|
273
|
+
# Handle single serializer
|
|
274
|
+
return resolved.generate_related_s()
|
|
156
275
|
|
|
157
276
|
@classmethod
|
|
158
277
|
def _is_special_field(
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Union
|
|
3
|
+
from django.test import TestCase, tag
|
|
4
|
+
|
|
5
|
+
from tests.test_app.models import TestModelForeignKey, TestModelReverseForeignKey
|
|
6
|
+
from ninja_aio.models import serializers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestModelForeignKeySerializer(serializers.Serializer):
|
|
10
|
+
class Meta:
|
|
11
|
+
model = TestModelForeignKey
|
|
12
|
+
schema_in = serializers.SchemaModelConfig(
|
|
13
|
+
fields=["name", "description", "test_model"]
|
|
14
|
+
)
|
|
15
|
+
schema_out = serializers.SchemaModelConfig(
|
|
16
|
+
fields=["id", "name", "description", "test_model"]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestModelReverseForeignKeySerializer(serializers.Serializer):
|
|
21
|
+
class Meta:
|
|
22
|
+
model = TestModelReverseForeignKey
|
|
23
|
+
schema_in = serializers.SchemaModelConfig(fields=["name", "description"])
|
|
24
|
+
schema_out = serializers.SchemaModelConfig(
|
|
25
|
+
fields=["id", "name", "description", "test_model_foreign_keys"]
|
|
26
|
+
)
|
|
27
|
+
relations_serializers = {
|
|
28
|
+
"test_model_foreign_keys": TestModelForeignKeySerializer,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Test serializers for Union tests - defined at module level so they can be resolved
|
|
33
|
+
class AltSerializer(serializers.Serializer):
|
|
34
|
+
class Meta:
|
|
35
|
+
model = TestModelForeignKey
|
|
36
|
+
schema_out = serializers.SchemaModelConfig(
|
|
37
|
+
fields=["id", "name"]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AltStringSerializer(serializers.Serializer):
|
|
42
|
+
class Meta:
|
|
43
|
+
model = TestModelForeignKey
|
|
44
|
+
schema_out = serializers.SchemaModelConfig(
|
|
45
|
+
fields=["id", "description"]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MixedAltSerializer(serializers.Serializer):
|
|
50
|
+
class Meta:
|
|
51
|
+
model = TestModelForeignKey
|
|
52
|
+
schema_out = serializers.SchemaModelConfig(
|
|
53
|
+
fields=["id", "name", "description"]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LocalTestSerializer(serializers.Serializer):
|
|
58
|
+
class Meta:
|
|
59
|
+
model = TestModelForeignKey
|
|
60
|
+
schema_out = serializers.SchemaModelConfig(fields=["id"])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@tag("serializers")
|
|
64
|
+
class SerializersTestCase(TestCase):
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def setUpTestData(cls):
|
|
68
|
+
cls.serializer_fk = TestModelForeignKeySerializer
|
|
69
|
+
cls.serializer_rfk = TestModelReverseForeignKeySerializer
|
|
70
|
+
warnings.simplefilter("ignore", UserWarning) # Ignora tutti i UserWarning
|
|
71
|
+
|
|
72
|
+
def test_generate_schema_out(self):
|
|
73
|
+
schema_out_fk = self.serializer_fk.generate_read_s()
|
|
74
|
+
for f in ["id", "name", "description", "test_model"]:
|
|
75
|
+
self.assertIn(f, schema_out_fk.model_fields)
|
|
76
|
+
|
|
77
|
+
schema_out_rfk = self.serializer_rfk.generate_read_s()
|
|
78
|
+
for f in ["id", "name", "description", "test_model_foreign_keys"]:
|
|
79
|
+
self.assertIn(f, schema_out_rfk.model_fields)
|
|
80
|
+
|
|
81
|
+
def test_generate_schema_in(self):
|
|
82
|
+
schema_in_fk = self.serializer_fk.generate_create_s()
|
|
83
|
+
# In schema should include declared input fields
|
|
84
|
+
for f in ["name", "description", "test_model"]:
|
|
85
|
+
self.assertIn(f, schema_in_fk.model_fields)
|
|
86
|
+
self.assertNotIn("id", schema_in_fk.model_fields)
|
|
87
|
+
|
|
88
|
+
schema_in_rfk = self.serializer_rfk.generate_create_s()
|
|
89
|
+
for f in ["name", "description"]:
|
|
90
|
+
self.assertIn(f, schema_in_rfk.model_fields)
|
|
91
|
+
|
|
92
|
+
def test_generate_schema_update(self):
|
|
93
|
+
# If no fields provided for update, optional fields should be honored when declared
|
|
94
|
+
# Here no explicit update config exists, so update schema may be None or empty depending on implementation
|
|
95
|
+
schema_patch_fk = self.serializer_fk.generate_update_s()
|
|
96
|
+
# Implementation returns a schema when fields/customs/excludes exist; otherwise may fallback to optionals.
|
|
97
|
+
# Our Meta doesn't define update, so ensure function doesn't crash and returns a Schema or None
|
|
98
|
+
self.assertTrue(
|
|
99
|
+
schema_patch_fk is None or hasattr(schema_patch_fk, "model_fields")
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
schema_patch_rfk = self.serializer_rfk.generate_update_s()
|
|
103
|
+
self.assertTrue(
|
|
104
|
+
schema_patch_rfk is None or hasattr(schema_patch_rfk, "model_fields")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def test_generate_related_schema(self):
|
|
108
|
+
related_fk = self.serializer_fk.generate_related_s()
|
|
109
|
+
# Related schema should include non-relational read fields only
|
|
110
|
+
for f in ["id", "name", "description"]:
|
|
111
|
+
self.assertIn(f, related_fk.model_fields)
|
|
112
|
+
# Forward relation declared on fk read fields should still be present as plain field in read, but not in related
|
|
113
|
+
self.assertNotIn("test_model", related_fk.model_fields)
|
|
114
|
+
|
|
115
|
+
related_rfk = self.serializer_rfk.generate_related_s()
|
|
116
|
+
for f in ["id", "name", "description"]:
|
|
117
|
+
self.assertIn(f, related_rfk.model_fields)
|
|
118
|
+
# Reverse relation should be excluded from related schema
|
|
119
|
+
self.assertNotIn("test_model_foreign_keys", related_rfk.model_fields)
|
|
120
|
+
|
|
121
|
+
def test_relation_serializer_required_when_mapping_provided(self):
|
|
122
|
+
# When relations_serializers mapping exists, any relation field listed in read fields must have a mapping
|
|
123
|
+
class BadSerializer(serializers.Serializer):
|
|
124
|
+
class Meta:
|
|
125
|
+
model = TestModelReverseForeignKey
|
|
126
|
+
schema_out = serializers.SchemaModelConfig(
|
|
127
|
+
fields=["id", "name", "description", "test_model_foreign_keys"]
|
|
128
|
+
)
|
|
129
|
+
relations_serializers = {}
|
|
130
|
+
|
|
131
|
+
def test_relation_serializer_inclusion(self):
|
|
132
|
+
# Ensure that providing relations_serializers yields nested related schema in read
|
|
133
|
+
schema_out_rfk = self.serializer_rfk.generate_read_s()
|
|
134
|
+
# The reverse relation should be represented as a field; the nested schema type comes from ninja's create_schema.
|
|
135
|
+
self.assertIn("test_model_foreign_keys", schema_out_rfk.model_fields)
|
|
136
|
+
# Also ensure base fields present
|
|
137
|
+
for f in ["id", "name", "description"]:
|
|
138
|
+
self.assertIn(f, schema_out_rfk.model_fields)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@tag("serializers", "union")
|
|
142
|
+
class UnionSerializerTestCase(TestCase):
|
|
143
|
+
"""Test cases for Union serializer references support."""
|
|
144
|
+
|
|
145
|
+
def setUp(self):
|
|
146
|
+
warnings.simplefilter("ignore", UserWarning)
|
|
147
|
+
|
|
148
|
+
def test_union_with_direct_class_references(self):
|
|
149
|
+
"""Test Union with direct class references."""
|
|
150
|
+
|
|
151
|
+
class UnionTestSerializer(serializers.Serializer):
|
|
152
|
+
class Meta:
|
|
153
|
+
model = TestModelReverseForeignKey
|
|
154
|
+
schema_out = serializers.SchemaModelConfig(
|
|
155
|
+
fields=["id", "name", "test_model_foreign_keys"]
|
|
156
|
+
)
|
|
157
|
+
relations_serializers = {
|
|
158
|
+
"test_model_foreign_keys": Union[TestModelForeignKeySerializer, AltSerializer],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Should resolve without errors
|
|
162
|
+
schema = UnionTestSerializer.generate_read_s()
|
|
163
|
+
self.assertIsNotNone(schema)
|
|
164
|
+
self.assertIn("test_model_foreign_keys", schema.model_fields)
|
|
165
|
+
|
|
166
|
+
def test_union_with_string_references(self):
|
|
167
|
+
"""Test Union with string references."""
|
|
168
|
+
|
|
169
|
+
class UnionStringTestSerializer(serializers.Serializer):
|
|
170
|
+
class Meta:
|
|
171
|
+
model = TestModelReverseForeignKey
|
|
172
|
+
schema_out = serializers.SchemaModelConfig(
|
|
173
|
+
fields=["id", "name", "test_model_foreign_keys"]
|
|
174
|
+
)
|
|
175
|
+
relations_serializers = {
|
|
176
|
+
"test_model_foreign_keys": Union["TestModelForeignKeySerializer", "AltStringSerializer"],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Should resolve without errors
|
|
180
|
+
schema = UnionStringTestSerializer.generate_read_s()
|
|
181
|
+
self.assertIsNotNone(schema)
|
|
182
|
+
self.assertIn("test_model_foreign_keys", schema.model_fields)
|
|
183
|
+
|
|
184
|
+
def test_union_with_mixed_references(self):
|
|
185
|
+
"""Test Union with mixed class and string references."""
|
|
186
|
+
|
|
187
|
+
class UnionMixedTestSerializer(serializers.Serializer):
|
|
188
|
+
class Meta:
|
|
189
|
+
model = TestModelReverseForeignKey
|
|
190
|
+
schema_out = serializers.SchemaModelConfig(
|
|
191
|
+
fields=["id", "name", "test_model_foreign_keys"]
|
|
192
|
+
)
|
|
193
|
+
relations_serializers = {
|
|
194
|
+
"test_model_foreign_keys": Union[MixedAltSerializer, "TestModelForeignKeySerializer"],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Should resolve without errors
|
|
198
|
+
schema = UnionMixedTestSerializer.generate_read_s()
|
|
199
|
+
self.assertIsNotNone(schema)
|
|
200
|
+
self.assertIn("test_model_foreign_keys", schema.model_fields)
|
|
201
|
+
|
|
202
|
+
def test_union_with_absolute_import_path(self):
|
|
203
|
+
"""Test Union with absolute import path string references."""
|
|
204
|
+
|
|
205
|
+
class UnionAbsolutePathSerializer(serializers.Serializer):
|
|
206
|
+
class Meta:
|
|
207
|
+
model = TestModelReverseForeignKey
|
|
208
|
+
schema_out = serializers.SchemaModelConfig(
|
|
209
|
+
fields=["id", "name", "test_model_foreign_keys"]
|
|
210
|
+
)
|
|
211
|
+
relations_serializers = {
|
|
212
|
+
"test_model_foreign_keys": Union[
|
|
213
|
+
"tests.test_serializers.TestModelForeignKeySerializer",
|
|
214
|
+
TestModelForeignKeySerializer,
|
|
215
|
+
],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Should resolve without errors
|
|
219
|
+
schema = UnionAbsolutePathSerializer.generate_read_s()
|
|
220
|
+
self.assertIsNotNone(schema)
|
|
221
|
+
self.assertIn("test_model_foreign_keys", schema.model_fields)
|
|
222
|
+
|
|
223
|
+
def test_resolve_serializer_reference_with_union(self):
|
|
224
|
+
"""Test _resolve_serializer_reference directly with Union types."""
|
|
225
|
+
from typing import get_args, get_origin
|
|
226
|
+
|
|
227
|
+
# Test with Union of DIFFERENT classes (Union of same type gets optimized away by Python)
|
|
228
|
+
union_ref = Union[TestModelForeignKeySerializer, AltSerializer]
|
|
229
|
+
resolved = TestModelForeignKeySerializer._resolve_serializer_reference(union_ref)
|
|
230
|
+
|
|
231
|
+
# Check that it returns a Union (using reduce with or_ creates a union-like structure)
|
|
232
|
+
# The resolved type should be a union of the two serializers
|
|
233
|
+
self.assertEqual(get_origin(resolved), Union)
|
|
234
|
+
resolved_args = get_args(resolved)
|
|
235
|
+
self.assertEqual(len(resolved_args), 2)
|
|
236
|
+
# Should contain both serializer classes
|
|
237
|
+
self.assertIn(TestModelForeignKeySerializer, resolved_args)
|
|
238
|
+
self.assertIn(AltSerializer, resolved_args)
|
|
239
|
+
|
|
240
|
+
def test_resolve_serializer_reference_with_string_union(self):
|
|
241
|
+
"""Test _resolve_serializer_reference with Union of strings."""
|
|
242
|
+
from typing import get_args, get_origin
|
|
243
|
+
|
|
244
|
+
# Test with Union of string references
|
|
245
|
+
union_ref = Union["TestModelForeignKeySerializer", "LocalTestSerializer"]
|
|
246
|
+
resolved = LocalTestSerializer._resolve_serializer_reference(union_ref)
|
|
247
|
+
|
|
248
|
+
# Check that it returns a Union
|
|
249
|
+
self.assertEqual(get_origin(resolved), Union)
|
|
250
|
+
resolved_types = get_args(resolved)
|
|
251
|
+
self.assertEqual(len(resolved_types), 2)
|
|
252
|
+
|
|
253
|
+
# Verify that both serializers are resolved correctly
|
|
254
|
+
self.assertIn(TestModelForeignKeySerializer, resolved_types)
|
|
255
|
+
self.assertIn(LocalTestSerializer, resolved_types)
|
|
256
|
+
|
|
257
|
+
def test_single_serializer_still_works(self):
|
|
258
|
+
"""Ensure single serializer references still work as before."""
|
|
259
|
+
|
|
260
|
+
class SingleRefSerializer(serializers.Serializer):
|
|
261
|
+
class Meta:
|
|
262
|
+
model = TestModelReverseForeignKey
|
|
263
|
+
schema_out = serializers.SchemaModelConfig(
|
|
264
|
+
fields=["id", "name", "test_model_foreign_keys"]
|
|
265
|
+
)
|
|
266
|
+
relations_serializers = {
|
|
267
|
+
"test_model_foreign_keys": TestModelForeignKeySerializer,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Should work exactly as before
|
|
271
|
+
schema = SingleRefSerializer.generate_read_s()
|
|
272
|
+
self.assertIsNotNone(schema)
|
|
273
|
+
self.assertIn("test_model_foreign_keys", schema.model_fields)
|
|
274
|
+
|
|
275
|
+
def test_single_string_serializer_still_works(self):
|
|
276
|
+
"""Ensure single string serializer references still work as before."""
|
|
277
|
+
|
|
278
|
+
class SingleStringRefSerializer(serializers.Serializer):
|
|
279
|
+
class Meta:
|
|
280
|
+
model = TestModelReverseForeignKey
|
|
281
|
+
schema_out = serializers.SchemaModelConfig(
|
|
282
|
+
fields=["id", "name", "test_model_foreign_keys"]
|
|
283
|
+
)
|
|
284
|
+
relations_serializers = {
|
|
285
|
+
"test_model_foreign_keys": "TestModelForeignKeySerializer",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Should work exactly as before
|
|
289
|
+
schema = SingleStringRefSerializer.generate_read_s()
|
|
290
|
+
self.assertIsNotNone(schema)
|
|
291
|
+
self.assertIn("test_model_foreign_keys", schema.model_fields)
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import warnings
|
|
2
|
-
from django.test import TestCase, tag
|
|
3
|
-
|
|
4
|
-
from tests.test_app.models import TestModelForeignKey, TestModelReverseForeignKey
|
|
5
|
-
from ninja_aio.models import serializers
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TestModelForeignKeySerializer(serializers.Serializer):
|
|
9
|
-
class Meta:
|
|
10
|
-
model = TestModelForeignKey
|
|
11
|
-
schema_in = serializers.SchemaModelConfig(
|
|
12
|
-
fields=["name", "description", "test_model"]
|
|
13
|
-
)
|
|
14
|
-
schema_out = serializers.SchemaModelConfig(
|
|
15
|
-
fields=["id", "name", "description", "test_model"]
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TestModelReverseForeignKeySerializer(serializers.Serializer):
|
|
20
|
-
class Meta:
|
|
21
|
-
model = TestModelReverseForeignKey
|
|
22
|
-
schema_in = serializers.SchemaModelConfig(fields=["name", "description"])
|
|
23
|
-
schema_out = serializers.SchemaModelConfig(
|
|
24
|
-
fields=["id", "name", "description", "test_model_foreign_keys"]
|
|
25
|
-
)
|
|
26
|
-
relations_serializers = {
|
|
27
|
-
"test_model_foreign_keys": TestModelForeignKeySerializer,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@tag("serializers")
|
|
32
|
-
class SerializersTestCase(TestCase):
|
|
33
|
-
|
|
34
|
-
@classmethod
|
|
35
|
-
def setUpTestData(cls):
|
|
36
|
-
cls.serializer_fk = TestModelForeignKeySerializer
|
|
37
|
-
cls.serializer_rfk = TestModelReverseForeignKeySerializer
|
|
38
|
-
warnings.simplefilter("ignore", UserWarning) # Ignora tutti i UserWarning
|
|
39
|
-
|
|
40
|
-
def test_generate_schema_out(self):
|
|
41
|
-
schema_out_fk = self.serializer_fk.generate_read_s()
|
|
42
|
-
for f in ["id", "name", "description", "test_model"]:
|
|
43
|
-
self.assertIn(f, schema_out_fk.model_fields)
|
|
44
|
-
|
|
45
|
-
schema_out_rfk = self.serializer_rfk.generate_read_s()
|
|
46
|
-
for f in ["id", "name", "description", "test_model_foreign_keys"]:
|
|
47
|
-
self.assertIn(f, schema_out_rfk.model_fields)
|
|
48
|
-
|
|
49
|
-
def test_generate_schema_in(self):
|
|
50
|
-
schema_in_fk = self.serializer_fk.generate_create_s()
|
|
51
|
-
# In schema should include declared input fields
|
|
52
|
-
for f in ["name", "description", "test_model"]:
|
|
53
|
-
self.assertIn(f, schema_in_fk.model_fields)
|
|
54
|
-
self.assertNotIn("id", schema_in_fk.model_fields)
|
|
55
|
-
|
|
56
|
-
schema_in_rfk = self.serializer_rfk.generate_create_s()
|
|
57
|
-
for f in ["name", "description"]:
|
|
58
|
-
self.assertIn(f, schema_in_rfk.model_fields)
|
|
59
|
-
|
|
60
|
-
def test_generate_schema_update(self):
|
|
61
|
-
# If no fields provided for update, optional fields should be honored when declared
|
|
62
|
-
# Here no explicit update config exists, so update schema may be None or empty depending on implementation
|
|
63
|
-
schema_patch_fk = self.serializer_fk.generate_update_s()
|
|
64
|
-
# Implementation returns a schema when fields/customs/excludes exist; otherwise may fallback to optionals.
|
|
65
|
-
# Our Meta doesn't define update, so ensure function doesn't crash and returns a Schema or None
|
|
66
|
-
self.assertTrue(
|
|
67
|
-
schema_patch_fk is None or hasattr(schema_patch_fk, "model_fields")
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
schema_patch_rfk = self.serializer_rfk.generate_update_s()
|
|
71
|
-
self.assertTrue(
|
|
72
|
-
schema_patch_rfk is None or hasattr(schema_patch_rfk, "model_fields")
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
def test_generate_related_schema(self):
|
|
76
|
-
related_fk = self.serializer_fk.generate_related_s()
|
|
77
|
-
# Related schema should include non-relational read fields only
|
|
78
|
-
for f in ["id", "name", "description"]:
|
|
79
|
-
self.assertIn(f, related_fk.model_fields)
|
|
80
|
-
# Forward relation declared on fk read fields should still be present as plain field in read, but not in related
|
|
81
|
-
self.assertNotIn("test_model", related_fk.model_fields)
|
|
82
|
-
|
|
83
|
-
related_rfk = self.serializer_rfk.generate_related_s()
|
|
84
|
-
for f in ["id", "name", "description"]:
|
|
85
|
-
self.assertIn(f, related_rfk.model_fields)
|
|
86
|
-
# Reverse relation should be excluded from related schema
|
|
87
|
-
self.assertNotIn("test_model_foreign_keys", related_rfk.model_fields)
|
|
88
|
-
|
|
89
|
-
def test_relation_serializer_required_when_mapping_provided(self):
|
|
90
|
-
# When relations_serializers mapping exists, any relation field listed in read fields must have a mapping
|
|
91
|
-
class BadSerializer(serializers.Serializer):
|
|
92
|
-
class Meta:
|
|
93
|
-
model = TestModelReverseForeignKey
|
|
94
|
-
schema_out = serializers.SchemaModelConfig(
|
|
95
|
-
fields=["id", "name", "description", "test_model_foreign_keys"]
|
|
96
|
-
)
|
|
97
|
-
relations_serializers = {}
|
|
98
|
-
|
|
99
|
-
def test_relation_serializer_inclusion(self):
|
|
100
|
-
# Ensure that providing relations_serializers yields nested related schema in read
|
|
101
|
-
schema_out_rfk = self.serializer_rfk.generate_read_s()
|
|
102
|
-
# The reverse relation should be represented as a field; the nested schema type comes from ninja's create_schema.
|
|
103
|
-
self.assertIn("test_model_foreign_keys", schema_out_rfk.model_fields)
|
|
104
|
-
# Also ensure base fields present
|
|
105
|
-
for f in ["id", "name", "description"]:
|
|
106
|
-
self.assertIn(f, schema_out_rfk.model_fields)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/models/model_serializer.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/api/renderers/orjson_renderer.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/installation.md
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/docs/getting_started/quick_start.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/__init__.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/ninja_aio/decorators/operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_exceptions_api.py
RENAMED
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/core/test_renderer_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/helpers/test_many_to_many_api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_ninja_aio_crud-2.6.1 → django_ninja_aio_crud-2.7.0}/tests/models/test_models_extra.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|