django-ninja-aio-crud 2.17.0__py3-none-any.whl → 2.18.0__py3-none-any.whl
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.
- django_ninja_aio_crud-2.18.0.dist-info/METADATA +425 -0
- {django_ninja_aio_crud-2.17.0.dist-info → django_ninja_aio_crud-2.18.0.dist-info}/RECORD +6 -6
- ninja_aio/__init__.py +1 -1
- ninja_aio/models/serializers.py +557 -47
- django_ninja_aio_crud-2.17.0.dist-info/METADATA +0 -379
- {django_ninja_aio_crud-2.17.0.dist-info → django_ninja_aio_crud-2.18.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.17.0.dist-info → django_ninja_aio_crud-2.18.0.dist-info}/licenses/LICENSE +0 -0
ninja_aio/models/serializers.py
CHANGED
|
@@ -25,6 +25,7 @@ from django.db.models.fields.related_descriptors import (
|
|
|
25
25
|
ForwardOneToOneDescriptor,
|
|
26
26
|
)
|
|
27
27
|
from pydantic import BeforeValidator, Field
|
|
28
|
+
from pydantic._internal._decorators import PydanticDescriptorProxy
|
|
28
29
|
|
|
29
30
|
from ninja_aio.types import (
|
|
30
31
|
S_TYPES,
|
|
@@ -102,14 +103,116 @@ class BaseSerializer:
|
|
|
102
103
|
queryset_request = ModelQuerySetSchema()
|
|
103
104
|
extras: list[ModelQuerySetExtraSchema] = []
|
|
104
105
|
|
|
106
|
+
@classmethod
|
|
107
|
+
def _collect_validators(cls, source_class) -> dict:
|
|
108
|
+
"""
|
|
109
|
+
Collect Pydantic validator descriptors from a class.
|
|
110
|
+
|
|
111
|
+
Iterates over the class attributes looking for ``PydanticDescriptorProxy``
|
|
112
|
+
instances (created by ``@field_validator`` and ``@model_validator`` decorators).
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
source_class : type | None
|
|
117
|
+
The class to scan for validators.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
dict
|
|
122
|
+
Mapping of attribute name to ``PydanticDescriptorProxy`` instance.
|
|
123
|
+
"""
|
|
124
|
+
validators = {}
|
|
125
|
+
if source_class is None:
|
|
126
|
+
return validators
|
|
127
|
+
for attr_name, attr_value in vars(source_class).items():
|
|
128
|
+
if isinstance(attr_value, PydanticDescriptorProxy):
|
|
129
|
+
validators[attr_name] = attr_value
|
|
130
|
+
return validators
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def _apply_validators(cls, schema, validators: dict):
|
|
134
|
+
"""
|
|
135
|
+
Create a subclass of the given schema with validators attached.
|
|
136
|
+
|
|
137
|
+
Pydantic discovers validators via ``PydanticDescriptorProxy`` instances
|
|
138
|
+
during class creation, so placing them on a subclass is sufficient.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
schema : Schema | None
|
|
143
|
+
The base schema class generated by ``create_schema``.
|
|
144
|
+
validators : dict
|
|
145
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
Schema | None
|
|
150
|
+
A subclass with validators applied, or the original schema if no
|
|
151
|
+
validators are provided.
|
|
152
|
+
"""
|
|
153
|
+
if not schema or not validators:
|
|
154
|
+
return schema
|
|
155
|
+
return type(schema.__name__, (schema,), validators)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def _get_validators(cls, schema_type: type[SCHEMA_TYPES]) -> dict:
|
|
159
|
+
"""
|
|
160
|
+
Return collected validators for the given schema type.
|
|
161
|
+
|
|
162
|
+
Subclasses must implement this to map schema types to the appropriate
|
|
163
|
+
validator source class.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
schema_type : SCHEMA_TYPES
|
|
168
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
dict
|
|
173
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
174
|
+
"""
|
|
175
|
+
return {}
|
|
176
|
+
|
|
105
177
|
@classmethod
|
|
106
178
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
107
|
-
|
|
179
|
+
"""
|
|
180
|
+
Return raw configuration list for the given serializer/field category.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
s_type : S_TYPES
|
|
185
|
+
Serializer type (``"create"`` | ``"update"`` | ``"read"`` | ``"detail"``).
|
|
186
|
+
f_type : F_TYPES
|
|
187
|
+
Field category (``"fields"`` | ``"optionals"`` | ``"customs"`` | ``"excludes"``).
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
list
|
|
192
|
+
Raw configuration list for the requested category.
|
|
193
|
+
|
|
194
|
+
Raises
|
|
195
|
+
------
|
|
196
|
+
NotImplementedError
|
|
197
|
+
Subclasses must provide an implementation.
|
|
198
|
+
"""
|
|
108
199
|
raise NotImplementedError
|
|
109
200
|
|
|
110
201
|
@classmethod
|
|
111
202
|
def _get_model(cls) -> models.Model:
|
|
112
|
-
|
|
203
|
+
"""
|
|
204
|
+
Return the Django model class associated with this serializer.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
models.Model
|
|
209
|
+
The Django model class.
|
|
210
|
+
|
|
211
|
+
Raises
|
|
212
|
+
------
|
|
213
|
+
NotImplementedError
|
|
214
|
+
Subclasses must provide an implementation.
|
|
215
|
+
"""
|
|
113
216
|
raise NotImplementedError
|
|
114
217
|
|
|
115
218
|
@classmethod
|
|
@@ -246,12 +349,32 @@ class BaseSerializer:
|
|
|
246
349
|
|
|
247
350
|
@classmethod
|
|
248
351
|
def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
|
|
249
|
-
|
|
352
|
+
"""
|
|
353
|
+
Return mapping of relation field names to their serializer classes.
|
|
354
|
+
|
|
355
|
+
Subclasses may override to provide explicit serializer mappings for
|
|
356
|
+
relation fields used during read schema generation.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
dict[str, Serializer]
|
|
361
|
+
Mapping of field name to serializer class. Empty by default.
|
|
362
|
+
"""
|
|
250
363
|
return {}
|
|
251
364
|
|
|
252
365
|
@classmethod
|
|
253
366
|
def _get_relations_as_id(cls) -> list[str]:
|
|
254
|
-
|
|
367
|
+
"""
|
|
368
|
+
Return relation field names that should be serialized as IDs.
|
|
369
|
+
|
|
370
|
+
Subclasses may override to specify which relation fields should be
|
|
371
|
+
represented as primary key values instead of nested objects.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
list[str]
|
|
376
|
+
Field names to serialize as IDs. Empty by default.
|
|
377
|
+
"""
|
|
255
378
|
return []
|
|
256
379
|
|
|
257
380
|
@classmethod
|
|
@@ -330,17 +453,50 @@ class BaseSerializer:
|
|
|
330
453
|
def _is_special_field(
|
|
331
454
|
cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
|
|
332
455
|
) -> bool:
|
|
333
|
-
"""
|
|
456
|
+
"""
|
|
457
|
+
Check whether a field appears in the given category for a serializer type.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
s_type : S_TYPES
|
|
462
|
+
Serializer type (``"create"`` | ``"update"`` | ``"read"`` | ``"detail"``).
|
|
463
|
+
field : str
|
|
464
|
+
The field name to look up.
|
|
465
|
+
f_type : F_TYPES
|
|
466
|
+
Field category (``"fields"`` | ``"optionals"`` | ``"customs"`` | ``"excludes"``).
|
|
467
|
+
|
|
468
|
+
Returns
|
|
469
|
+
-------
|
|
470
|
+
bool
|
|
471
|
+
``True`` if the field is found in the specified category.
|
|
472
|
+
"""
|
|
334
473
|
special_fields = cls._get_fields(s_type, f_type)
|
|
335
474
|
return any(field in special_f for special_f in special_fields)
|
|
336
475
|
|
|
337
476
|
@classmethod
|
|
338
477
|
def get_custom_fields(cls, s_type: type[S_TYPES]) -> list[tuple[str, type, Any]]:
|
|
339
478
|
"""
|
|
340
|
-
Normalize declared custom field specs into (name, py_type, default).
|
|
479
|
+
Normalize declared custom field specs into ``(name, py_type, default)`` tuples.
|
|
480
|
+
|
|
341
481
|
Accepted tuple shapes:
|
|
342
|
-
|
|
343
|
-
- (name, py_type)
|
|
482
|
+
|
|
483
|
+
- ``(name, py_type, default)`` -- field with explicit default.
|
|
484
|
+
- ``(name, py_type)`` -- required field (default set to ``Ellipsis``).
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
s_type : S_TYPES
|
|
489
|
+
Serializer type whose custom fields to retrieve.
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
list[tuple[str, type, Any]]
|
|
494
|
+
Normalized list of ``(name, type, default)`` tuples.
|
|
495
|
+
|
|
496
|
+
Raises
|
|
497
|
+
------
|
|
498
|
+
ValueError
|
|
499
|
+
If a custom field spec is not a tuple or has an invalid length.
|
|
344
500
|
"""
|
|
345
501
|
raw_customs = cls._get_fields(s_type, "customs") or []
|
|
346
502
|
normalized: list[tuple[str, Any, Any]] = []
|
|
@@ -362,7 +518,19 @@ class BaseSerializer:
|
|
|
362
518
|
|
|
363
519
|
@classmethod
|
|
364
520
|
def get_optional_fields(cls, s_type: type[S_TYPES]):
|
|
365
|
-
"""
|
|
521
|
+
"""
|
|
522
|
+
Return optional field specs normalized to ``(name, type, None)`` tuples.
|
|
523
|
+
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
s_type : S_TYPES
|
|
527
|
+
Serializer type whose optional fields to retrieve.
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
list[tuple[str, type, None]]
|
|
532
|
+
Normalized list where each entry defaults to ``None``.
|
|
533
|
+
"""
|
|
366
534
|
return [
|
|
367
535
|
(field, field_type, None)
|
|
368
536
|
for field, field_type in cls._get_fields(s_type, "optionals")
|
|
@@ -370,12 +538,39 @@ class BaseSerializer:
|
|
|
370
538
|
|
|
371
539
|
@classmethod
|
|
372
540
|
def get_excluded_fields(cls, s_type: S_TYPES):
|
|
373
|
-
"""
|
|
541
|
+
"""
|
|
542
|
+
Return excluded field names for the given serializer type.
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
s_type : S_TYPES
|
|
547
|
+
Serializer type whose exclusions to retrieve.
|
|
548
|
+
|
|
549
|
+
Returns
|
|
550
|
+
-------
|
|
551
|
+
list[str]
|
|
552
|
+
Field names excluded from schema generation.
|
|
553
|
+
"""
|
|
374
554
|
return cls._get_fields(s_type, "excludes")
|
|
375
555
|
|
|
376
556
|
@classmethod
|
|
377
557
|
def get_fields(cls, s_type: S_TYPES):
|
|
378
|
-
"""
|
|
558
|
+
"""
|
|
559
|
+
Return explicit declared field names for the serializer type.
|
|
560
|
+
|
|
561
|
+
Filters out inline custom field tuples from the fields list, returning
|
|
562
|
+
only string field names.
|
|
563
|
+
|
|
564
|
+
Parameters
|
|
565
|
+
----------
|
|
566
|
+
s_type : S_TYPES
|
|
567
|
+
Serializer type whose fields to retrieve.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
list[str]
|
|
572
|
+
Model field names (excludes inline custom tuples).
|
|
573
|
+
"""
|
|
379
574
|
fields = cls._get_fields(s_type, "fields")
|
|
380
575
|
# Filter out inline custom field tuples, return only string field names
|
|
381
576
|
return [f for f in fields if isinstance(f, str)]
|
|
@@ -411,14 +606,40 @@ class BaseSerializer:
|
|
|
411
606
|
|
|
412
607
|
@classmethod
|
|
413
608
|
def is_custom(cls, field: str) -> bool:
|
|
414
|
-
"""
|
|
609
|
+
"""
|
|
610
|
+
Check if a field is declared as a custom input in create or update.
|
|
611
|
+
|
|
612
|
+
Parameters
|
|
613
|
+
----------
|
|
614
|
+
field : str
|
|
615
|
+
The field name to check.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
bool
|
|
620
|
+
``True`` if the field appears in the customs category for either
|
|
621
|
+
``create`` or ``update`` serializer types.
|
|
622
|
+
"""
|
|
415
623
|
return cls._is_special_field(
|
|
416
624
|
"create", field, "customs"
|
|
417
625
|
) or cls._is_special_field("update", field, "customs")
|
|
418
626
|
|
|
419
627
|
@classmethod
|
|
420
628
|
def is_optional(cls, field: str) -> bool:
|
|
421
|
-
"""
|
|
629
|
+
"""
|
|
630
|
+
Check if a field is declared as optional in create or update.
|
|
631
|
+
|
|
632
|
+
Parameters
|
|
633
|
+
----------
|
|
634
|
+
field : str
|
|
635
|
+
The field name to check.
|
|
636
|
+
|
|
637
|
+
Returns
|
|
638
|
+
-------
|
|
639
|
+
bool
|
|
640
|
+
``True`` if the field appears in the optionals category for either
|
|
641
|
+
``create`` or ``update`` serializer types.
|
|
642
|
+
"""
|
|
422
643
|
return cls._is_special_field(
|
|
423
644
|
"create", field, "optionals"
|
|
424
645
|
) or cls._is_special_field("update", field, "optionals")
|
|
@@ -531,7 +752,20 @@ class BaseSerializer:
|
|
|
531
752
|
|
|
532
753
|
@classmethod
|
|
533
754
|
def _is_reverse_relation(cls, field_obj) -> bool:
|
|
534
|
-
"""
|
|
755
|
+
"""
|
|
756
|
+
Check if a field descriptor represents a reverse relation.
|
|
757
|
+
|
|
758
|
+
Parameters
|
|
759
|
+
----------
|
|
760
|
+
field_obj : Any
|
|
761
|
+
Django model field descriptor.
|
|
762
|
+
|
|
763
|
+
Returns
|
|
764
|
+
-------
|
|
765
|
+
bool
|
|
766
|
+
``True`` if the descriptor is a ``ManyToManyDescriptor``,
|
|
767
|
+
``ReverseManyToOneDescriptor``, or ``ReverseOneToOneDescriptor``.
|
|
768
|
+
"""
|
|
535
769
|
return isinstance(
|
|
536
770
|
field_obj,
|
|
537
771
|
(
|
|
@@ -543,14 +777,39 @@ class BaseSerializer:
|
|
|
543
777
|
|
|
544
778
|
@classmethod
|
|
545
779
|
def _is_forward_relation(cls, field_obj) -> bool:
|
|
546
|
-
"""
|
|
780
|
+
"""
|
|
781
|
+
Check if a field descriptor represents a forward relation.
|
|
782
|
+
|
|
783
|
+
Parameters
|
|
784
|
+
----------
|
|
785
|
+
field_obj : Any
|
|
786
|
+
Django model field descriptor.
|
|
787
|
+
|
|
788
|
+
Returns
|
|
789
|
+
-------
|
|
790
|
+
bool
|
|
791
|
+
``True`` if the descriptor is a ``ForwardOneToOneDescriptor``
|
|
792
|
+
or ``ForwardManyToOneDescriptor``.
|
|
793
|
+
"""
|
|
547
794
|
return isinstance(
|
|
548
795
|
field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
|
|
549
796
|
)
|
|
550
797
|
|
|
551
798
|
@classmethod
|
|
552
799
|
def _warn_missing_relation_serializer(cls, field_name: str, model) -> None:
|
|
553
|
-
"""
|
|
800
|
+
"""
|
|
801
|
+
Emit a warning for reverse relations without an explicit serializer mapping.
|
|
802
|
+
|
|
803
|
+
Only warns when the related model is not a ``ModelSerializer`` and the
|
|
804
|
+
``NINJA_AIO_RAISE_SERIALIZATION_WARNINGS`` setting is enabled (default).
|
|
805
|
+
|
|
806
|
+
Parameters
|
|
807
|
+
----------
|
|
808
|
+
field_name : str
|
|
809
|
+
Name of the reverse relation field.
|
|
810
|
+
model : type
|
|
811
|
+
The Django model class owning the field.
|
|
812
|
+
"""
|
|
554
813
|
if not isinstance(model, ModelSerializerMeta) and getattr(
|
|
555
814
|
settings, "NINJA_AIO_RAISE_SERIALIZATION_WARNINGS", True
|
|
556
815
|
):
|
|
@@ -675,10 +934,26 @@ class BaseSerializer:
|
|
|
675
934
|
depth: int = None,
|
|
676
935
|
) -> Schema:
|
|
677
936
|
"""
|
|
678
|
-
Core schema factory bridging configuration to ninja.orm.create_schema
|
|
679
|
-
|
|
937
|
+
Core schema factory bridging serializer configuration to ``ninja.orm.create_schema``.
|
|
938
|
+
|
|
939
|
+
Dispatches to the appropriate field/custom/exclude gathering logic based
|
|
940
|
+
on the requested schema type and delegates to Django Ninja's
|
|
941
|
+
``create_schema`` for the actual Pydantic model construction.
|
|
942
|
+
|
|
943
|
+
Parameters
|
|
944
|
+
----------
|
|
945
|
+
schema_type : SCHEMA_TYPES
|
|
946
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
947
|
+
depth : int, optional
|
|
948
|
+
Nesting depth for related model schemas (used by ``Out`` and ``Detail``).
|
|
949
|
+
|
|
950
|
+
Returns
|
|
951
|
+
-------
|
|
952
|
+
Schema | None
|
|
953
|
+
Generated Pydantic schema, or ``None`` if no fields are configured.
|
|
680
954
|
"""
|
|
681
955
|
model = cls._get_model()
|
|
956
|
+
validators = cls._get_validators(schema_type)
|
|
682
957
|
|
|
683
958
|
# Handle special schema types with custom logic
|
|
684
959
|
if schema_type == "Out" or schema_type == "Detail":
|
|
@@ -688,7 +963,7 @@ class BaseSerializer:
|
|
|
688
963
|
if not any([fields, reverse_rels, excludes, customs]):
|
|
689
964
|
return None
|
|
690
965
|
schema_name = "SchemaOut" if schema_type == "Out" else "DetailSchemaOut"
|
|
691
|
-
|
|
966
|
+
schema = create_schema(
|
|
692
967
|
model=model,
|
|
693
968
|
name=f"{model._meta.model_name}{schema_name}",
|
|
694
969
|
depth=depth,
|
|
@@ -696,26 +971,26 @@ class BaseSerializer:
|
|
|
696
971
|
custom_fields=reverse_rels + customs + optionals,
|
|
697
972
|
exclude=excludes,
|
|
698
973
|
)
|
|
974
|
+
return cls._apply_validators(schema, validators)
|
|
699
975
|
|
|
700
976
|
if schema_type == "Related":
|
|
701
977
|
fields, customs = cls.get_related_schema_data()
|
|
702
978
|
if not fields and not customs:
|
|
703
979
|
return None
|
|
704
|
-
|
|
980
|
+
schema = create_schema(
|
|
705
981
|
model=model,
|
|
706
982
|
name=f"{model._meta.model_name}SchemaRelated",
|
|
707
983
|
fields=fields,
|
|
708
984
|
custom_fields=customs,
|
|
709
985
|
)
|
|
986
|
+
return cls._apply_validators(schema, validators)
|
|
710
987
|
|
|
711
988
|
# Handle standard In/Patch schema types
|
|
712
989
|
s_type = "create" if schema_type == "In" else "update"
|
|
713
990
|
fields = cls.get_fields(s_type)
|
|
714
991
|
optionals = cls.get_optional_fields(s_type)
|
|
715
992
|
customs = (
|
|
716
|
-
cls.get_custom_fields(s_type)
|
|
717
|
-
+ optionals
|
|
718
|
-
+ cls.get_inline_customs(s_type)
|
|
993
|
+
cls.get_custom_fields(s_type) + optionals + cls.get_inline_customs(s_type)
|
|
719
994
|
)
|
|
720
995
|
excludes = cls.get_excluded_fields(s_type)
|
|
721
996
|
|
|
@@ -736,13 +1011,14 @@ class BaseSerializer:
|
|
|
736
1011
|
if not any([fields, customs, excludes]):
|
|
737
1012
|
return None
|
|
738
1013
|
|
|
739
|
-
|
|
1014
|
+
schema = create_schema(
|
|
740
1015
|
model=model,
|
|
741
1016
|
name=f"{model._meta.model_name}Schema{schema_type}",
|
|
742
1017
|
fields=fields,
|
|
743
1018
|
custom_fields=customs,
|
|
744
1019
|
exclude=excludes,
|
|
745
1020
|
)
|
|
1021
|
+
return cls._apply_validators(schema, validators)
|
|
746
1022
|
|
|
747
1023
|
@classmethod
|
|
748
1024
|
def get_related_schema_data(cls):
|
|
@@ -780,27 +1056,78 @@ class BaseSerializer:
|
|
|
780
1056
|
|
|
781
1057
|
@classmethod
|
|
782
1058
|
def generate_read_s(cls, depth: int = 1) -> Schema:
|
|
783
|
-
"""
|
|
1059
|
+
"""
|
|
1060
|
+
Generate the read (Out) schema for list responses.
|
|
1061
|
+
|
|
1062
|
+
Parameters
|
|
1063
|
+
----------
|
|
1064
|
+
depth : int, optional
|
|
1065
|
+
Nesting depth for related models. Defaults to ``1``.
|
|
1066
|
+
|
|
1067
|
+
Returns
|
|
1068
|
+
-------
|
|
1069
|
+
Schema | None
|
|
1070
|
+
Generated Pydantic schema, or ``None`` if no read fields are configured.
|
|
1071
|
+
"""
|
|
784
1072
|
return cls._generate_model_schema("Out", depth)
|
|
785
1073
|
|
|
786
1074
|
@classmethod
|
|
787
1075
|
def generate_detail_s(cls, depth: int = 1) -> Schema:
|
|
788
|
-
"""
|
|
1076
|
+
"""
|
|
1077
|
+
Generate the detail (single-object) read schema.
|
|
1078
|
+
|
|
1079
|
+
Falls back to the standard read schema if no detail-specific
|
|
1080
|
+
configuration is defined.
|
|
1081
|
+
|
|
1082
|
+
Parameters
|
|
1083
|
+
----------
|
|
1084
|
+
depth : int, optional
|
|
1085
|
+
Nesting depth for related models. Defaults to ``1``.
|
|
1086
|
+
|
|
1087
|
+
Returns
|
|
1088
|
+
-------
|
|
1089
|
+
Schema
|
|
1090
|
+
Generated Pydantic schema (never ``None``; falls back to read schema).
|
|
1091
|
+
"""
|
|
789
1092
|
return cls._generate_model_schema("Detail", depth) or cls.generate_read_s(depth)
|
|
790
1093
|
|
|
791
1094
|
@classmethod
|
|
792
1095
|
def generate_create_s(cls) -> Schema:
|
|
793
|
-
"""
|
|
1096
|
+
"""
|
|
1097
|
+
Generate the create (In) schema for input validation.
|
|
1098
|
+
|
|
1099
|
+
Returns
|
|
1100
|
+
-------
|
|
1101
|
+
Schema | None
|
|
1102
|
+
Generated Pydantic schema, or ``None`` if no create fields are configured.
|
|
1103
|
+
"""
|
|
794
1104
|
return cls._generate_model_schema("In")
|
|
795
1105
|
|
|
796
1106
|
@classmethod
|
|
797
1107
|
def generate_update_s(cls) -> Schema:
|
|
798
|
-
"""
|
|
1108
|
+
"""
|
|
1109
|
+
Generate the update (Patch) schema for partial updates.
|
|
1110
|
+
|
|
1111
|
+
Returns
|
|
1112
|
+
-------
|
|
1113
|
+
Schema | None
|
|
1114
|
+
Generated Pydantic schema, or ``None`` if no update fields are configured.
|
|
1115
|
+
"""
|
|
799
1116
|
return cls._generate_model_schema("Patch")
|
|
800
1117
|
|
|
801
1118
|
@classmethod
|
|
802
1119
|
def generate_related_s(cls) -> Schema:
|
|
803
|
-
"""
|
|
1120
|
+
"""
|
|
1121
|
+
Generate the related (nested) schema for embedding in parent schemas.
|
|
1122
|
+
|
|
1123
|
+
Includes only non-relational model fields and custom fields, preventing
|
|
1124
|
+
infinite nesting of related objects.
|
|
1125
|
+
|
|
1126
|
+
Returns
|
|
1127
|
+
-------
|
|
1128
|
+
Schema | None
|
|
1129
|
+
Generated Pydantic schema, or ``None`` if no fields are configured.
|
|
1130
|
+
"""
|
|
804
1131
|
return cls._generate_model_schema("Related")
|
|
805
1132
|
|
|
806
1133
|
@classmethod
|
|
@@ -861,8 +1188,8 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
861
1188
|
Disallowed model fields on create (e.g., id, timestamps).
|
|
862
1189
|
"""
|
|
863
1190
|
|
|
864
|
-
fields: list[str | tuple[str, Any, Any]] = []
|
|
865
|
-
customs: list[tuple[str, Any, Any]] = []
|
|
1191
|
+
fields: list[str | tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
1192
|
+
customs: list[tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
866
1193
|
optionals: list[tuple[str, Any]] = []
|
|
867
1194
|
excludes: list[str] = []
|
|
868
1195
|
|
|
@@ -883,8 +1210,8 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
883
1210
|
Relation fields to serialize as IDs instead of nested objects.
|
|
884
1211
|
"""
|
|
885
1212
|
|
|
886
|
-
fields: list[str | tuple[str, Any, Any]] = []
|
|
887
|
-
customs: list[tuple[str, Any, Any]] = []
|
|
1213
|
+
fields: list[str | tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
1214
|
+
customs: list[tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
888
1215
|
optionals: list[tuple[str, Any]] = []
|
|
889
1216
|
excludes: list[str] = []
|
|
890
1217
|
relations_as_id: list[str] = []
|
|
@@ -904,8 +1231,8 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
904
1231
|
Optional output fields.
|
|
905
1232
|
"""
|
|
906
1233
|
|
|
907
|
-
fields: list[str | tuple[str, Any, Any]] = []
|
|
908
|
-
customs: list[tuple[str, Any, Any]] = []
|
|
1234
|
+
fields: list[str | tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
1235
|
+
customs: list[tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
909
1236
|
optionals: list[tuple[str, Any]] = []
|
|
910
1237
|
excludes: list[str] = []
|
|
911
1238
|
|
|
@@ -937,9 +1264,47 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
937
1264
|
"detail": "DetailSerializer",
|
|
938
1265
|
}
|
|
939
1266
|
|
|
1267
|
+
# Schema type to serializer type mapping for validator resolution
|
|
1268
|
+
_SCHEMA_TO_S_TYPE = {
|
|
1269
|
+
"In": "create",
|
|
1270
|
+
"Patch": "update",
|
|
1271
|
+
"Out": "read",
|
|
1272
|
+
"Detail": "detail",
|
|
1273
|
+
"Related": "read",
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
@classmethod
|
|
1277
|
+
def _get_validators(cls, schema_type: type[SCHEMA_TYPES]) -> dict:
|
|
1278
|
+
"""
|
|
1279
|
+
Collect validators from the inner serializer class for the given schema type.
|
|
1280
|
+
|
|
1281
|
+
Parameters
|
|
1282
|
+
----------
|
|
1283
|
+
schema_type : SCHEMA_TYPES
|
|
1284
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
1285
|
+
|
|
1286
|
+
Returns
|
|
1287
|
+
-------
|
|
1288
|
+
dict
|
|
1289
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
1290
|
+
"""
|
|
1291
|
+
s_type = cls._SCHEMA_TO_S_TYPE.get(schema_type)
|
|
1292
|
+
config_name = cls._SERIALIZER_CONFIG_MAP.get(s_type)
|
|
1293
|
+
config_class = getattr(cls, config_name, None) if config_name else None
|
|
1294
|
+
return cls._collect_validators(config_class)
|
|
1295
|
+
|
|
940
1296
|
@classmethod
|
|
941
1297
|
def _get_relations_as_id(cls) -> list[str]:
|
|
942
|
-
"""
|
|
1298
|
+
"""
|
|
1299
|
+
Return relation fields to serialize as primary key values.
|
|
1300
|
+
|
|
1301
|
+
Reads the ``relations_as_id`` attribute from ``ReadSerializer``.
|
|
1302
|
+
|
|
1303
|
+
Returns
|
|
1304
|
+
-------
|
|
1305
|
+
list[str]
|
|
1306
|
+
Field names whose related objects should be serialized as IDs.
|
|
1307
|
+
"""
|
|
943
1308
|
return getattr(cls.ReadSerializer, "relations_as_id", [])
|
|
944
1309
|
|
|
945
1310
|
@classmethod
|
|
@@ -970,17 +1335,30 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
970
1335
|
|
|
971
1336
|
@classmethod
|
|
972
1337
|
def _get_model(cls) -> "ModelSerializer":
|
|
973
|
-
"""
|
|
1338
|
+
"""
|
|
1339
|
+
Return the model class itself.
|
|
1340
|
+
|
|
1341
|
+
Since ``ModelSerializer`` is mixed directly into the model, the model
|
|
1342
|
+
class is the serializer class.
|
|
1343
|
+
|
|
1344
|
+
Returns
|
|
1345
|
+
-------
|
|
1346
|
+
ModelSerializer
|
|
1347
|
+
The model/serializer class.
|
|
1348
|
+
"""
|
|
974
1349
|
return cls
|
|
975
1350
|
|
|
976
1351
|
@classmethod
|
|
977
1352
|
def verbose_name_path_resolver(cls) -> str:
|
|
978
1353
|
"""
|
|
979
|
-
Slugify plural verbose name for URL path segment.
|
|
1354
|
+
Slugify the plural verbose name for use as a URL path segment.
|
|
1355
|
+
|
|
1356
|
+
Replaces spaces with hyphens in the model's ``verbose_name_plural``.
|
|
980
1357
|
|
|
981
1358
|
Returns
|
|
982
1359
|
-------
|
|
983
1360
|
str
|
|
1361
|
+
Hyphen-separated URL-safe path segment.
|
|
984
1362
|
"""
|
|
985
1363
|
return "-".join(cls._meta.verbose_name_plural.split(" "))
|
|
986
1364
|
|
|
@@ -1103,14 +1481,16 @@ class SchemaModelConfig(Schema):
|
|
|
1103
1481
|
Optional model fields. Type can be any valid type annotation including Union.
|
|
1104
1482
|
exclude : Optional[List[str]]
|
|
1105
1483
|
Model fields to exclude.
|
|
1106
|
-
customs : Optional[List[tuple[str, Any, Any]]]
|
|
1484
|
+
customs : Optional[List[tuple[str, Any, Any] | tuple[str, Any]]]
|
|
1107
1485
|
Custom / synthetic fields. Type can be any valid type annotation including Union.
|
|
1486
|
+
- 2-tuple: (name, type) - required field
|
|
1487
|
+
- 3-tuple: (name, type, default) - optional field with default
|
|
1108
1488
|
"""
|
|
1109
1489
|
|
|
1110
1490
|
fields: Optional[List[str | tuple[str, Any, Any] | tuple[str, Any]]] = None
|
|
1111
1491
|
optionals: Optional[List[tuple[str, Any]]] = None
|
|
1112
1492
|
exclude: Optional[List[str]] = None
|
|
1113
|
-
customs: Optional[List[tuple[str, Any, Any]]] = None
|
|
1493
|
+
customs: Optional[List[tuple[str, Any, Any] | tuple[str, Any]]] = None
|
|
1114
1494
|
|
|
1115
1495
|
|
|
1116
1496
|
class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
@@ -1131,6 +1511,15 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1131
1511
|
"detail": "detail",
|
|
1132
1512
|
}
|
|
1133
1513
|
|
|
1514
|
+
# Schema type to validators inner class mapping
|
|
1515
|
+
_VALIDATORS_CLASS_MAP = {
|
|
1516
|
+
"In": "CreateValidators",
|
|
1517
|
+
"Patch": "UpdateValidators",
|
|
1518
|
+
"Out": "ReadValidators",
|
|
1519
|
+
"Detail": "DetailValidators",
|
|
1520
|
+
"Related": "ReadValidators",
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1134
1523
|
def __init_subclass__(cls, **kwargs):
|
|
1135
1524
|
super().__init_subclass__(**kwargs)
|
|
1136
1525
|
from ninja_aio.models.utils import ModelUtil
|
|
@@ -1150,26 +1539,118 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1150
1539
|
relations_serializers: dict[str, "Serializer"] = {}
|
|
1151
1540
|
relations_as_id: list[str] = []
|
|
1152
1541
|
|
|
1542
|
+
def _parse_payload(self, payload: dict[str, Any] | Schema) -> dict[str, Any]:
|
|
1543
|
+
"""
|
|
1544
|
+
Parse and return the input payload.
|
|
1545
|
+
|
|
1546
|
+
Can be overridden to implement custom parsing logic.
|
|
1547
|
+
|
|
1548
|
+
Parameters
|
|
1549
|
+
----------
|
|
1550
|
+
payload : dict | Schema
|
|
1551
|
+
Input data.
|
|
1552
|
+
|
|
1553
|
+
Returns
|
|
1554
|
+
-------
|
|
1555
|
+
dict
|
|
1556
|
+
Parsed payload.
|
|
1557
|
+
"""
|
|
1558
|
+
return payload.model_dump() if isinstance(payload, Schema) else payload
|
|
1559
|
+
|
|
1560
|
+
@classmethod
|
|
1561
|
+
def _get_validators(cls, schema_type: type[SCHEMA_TYPES]) -> dict:
|
|
1562
|
+
"""
|
|
1563
|
+
Collect validators from the inner validators class for the given schema type.
|
|
1564
|
+
|
|
1565
|
+
Looks for inner classes named ``CreateValidators``, ``ReadValidators``,
|
|
1566
|
+
``UpdateValidators``, or ``DetailValidators`` on the serializer.
|
|
1567
|
+
|
|
1568
|
+
Parameters
|
|
1569
|
+
----------
|
|
1570
|
+
schema_type : SCHEMA_TYPES
|
|
1571
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
1572
|
+
|
|
1573
|
+
Returns
|
|
1574
|
+
-------
|
|
1575
|
+
dict
|
|
1576
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
1577
|
+
"""
|
|
1578
|
+
class_name = cls._VALIDATORS_CLASS_MAP.get(schema_type)
|
|
1579
|
+
validators_class = getattr(cls, class_name, None) if class_name else None
|
|
1580
|
+
return cls._collect_validators(validators_class)
|
|
1581
|
+
|
|
1153
1582
|
@classmethod
|
|
1154
1583
|
def _get_relations_as_id(cls) -> list[str]:
|
|
1584
|
+
"""
|
|
1585
|
+
Return relation fields to serialize as primary key values.
|
|
1586
|
+
|
|
1587
|
+
Reads the ``relations_as_id`` attribute from ``Meta``.
|
|
1588
|
+
|
|
1589
|
+
Returns
|
|
1590
|
+
-------
|
|
1591
|
+
list[str]
|
|
1592
|
+
Field names whose related objects should be serialized as IDs.
|
|
1593
|
+
"""
|
|
1155
1594
|
relations_as_id = cls._get_meta_data("relations_as_id")
|
|
1156
1595
|
return relations_as_id or []
|
|
1157
1596
|
|
|
1158
1597
|
@classmethod
|
|
1159
1598
|
def _get_meta_data(cls, attr_name: str) -> Any:
|
|
1599
|
+
"""
|
|
1600
|
+
Retrieve an attribute from the nested ``Meta`` class.
|
|
1601
|
+
|
|
1602
|
+
Parameters
|
|
1603
|
+
----------
|
|
1604
|
+
attr_name : str
|
|
1605
|
+
Name of the ``Meta`` attribute to look up.
|
|
1606
|
+
|
|
1607
|
+
Returns
|
|
1608
|
+
-------
|
|
1609
|
+
Any
|
|
1610
|
+
The attribute value, or ``None`` if not defined.
|
|
1611
|
+
"""
|
|
1160
1612
|
return getattr(cls.Meta, attr_name, None)
|
|
1161
1613
|
|
|
1162
1614
|
@classmethod
|
|
1163
1615
|
def _get_model(cls) -> models.Model:
|
|
1616
|
+
"""
|
|
1617
|
+
Return the Django model class from ``Meta.model``.
|
|
1618
|
+
|
|
1619
|
+
Returns
|
|
1620
|
+
-------
|
|
1621
|
+
models.Model
|
|
1622
|
+
The validated Django model class.
|
|
1623
|
+
"""
|
|
1164
1624
|
return cls._validate_model()
|
|
1165
1625
|
|
|
1166
1626
|
@classmethod
|
|
1167
1627
|
def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
|
|
1628
|
+
"""
|
|
1629
|
+
Return the explicit relation-to-serializer mapping from ``Meta``.
|
|
1630
|
+
|
|
1631
|
+
Returns
|
|
1632
|
+
-------
|
|
1633
|
+
dict[str, Serializer]
|
|
1634
|
+
Mapping of relation field names to serializer classes.
|
|
1635
|
+
"""
|
|
1168
1636
|
relations_serializers = cls._get_meta_data("relations_serializers")
|
|
1169
1637
|
return relations_serializers or {}
|
|
1170
1638
|
|
|
1171
1639
|
@classmethod
|
|
1172
1640
|
def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig | None:
|
|
1641
|
+
"""
|
|
1642
|
+
Retrieve the ``SchemaModelConfig`` for the given schema type.
|
|
1643
|
+
|
|
1644
|
+
Parameters
|
|
1645
|
+
----------
|
|
1646
|
+
schema_type : str
|
|
1647
|
+
One of ``"in"``, ``"out"``, ``"update"``, or ``"detail"``.
|
|
1648
|
+
|
|
1649
|
+
Returns
|
|
1650
|
+
-------
|
|
1651
|
+
SchemaModelConfig | None
|
|
1652
|
+
The configuration object, or ``None`` if not defined.
|
|
1653
|
+
"""
|
|
1173
1654
|
match schema_type:
|
|
1174
1655
|
case "in":
|
|
1175
1656
|
return cls._get_meta_data("schema_in")
|
|
@@ -1184,6 +1665,19 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1184
1665
|
|
|
1185
1666
|
@classmethod
|
|
1186
1667
|
def _validate_model(cls):
|
|
1668
|
+
"""
|
|
1669
|
+
Validate and return the model defined in ``Meta.model``.
|
|
1670
|
+
|
|
1671
|
+
Returns
|
|
1672
|
+
-------
|
|
1673
|
+
models.Model
|
|
1674
|
+
The validated Django model class.
|
|
1675
|
+
|
|
1676
|
+
Raises
|
|
1677
|
+
------
|
|
1678
|
+
ValueError
|
|
1679
|
+
If ``Meta.model`` is not defined or is not a Django model subclass.
|
|
1680
|
+
"""
|
|
1187
1681
|
model = cls._get_meta_data("model")
|
|
1188
1682
|
if not model:
|
|
1189
1683
|
raise ValueError("Meta.model must be defined for Serializer.")
|
|
@@ -1193,7 +1687,23 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1193
1687
|
|
|
1194
1688
|
@classmethod
|
|
1195
1689
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
1196
|
-
"""
|
|
1690
|
+
"""
|
|
1691
|
+
Return raw configuration list from the Meta schema for the given categories.
|
|
1692
|
+
|
|
1693
|
+
Falls back to the ``out`` schema when ``detail`` is requested but not defined.
|
|
1694
|
+
|
|
1695
|
+
Parameters
|
|
1696
|
+
----------
|
|
1697
|
+
s_type : S_TYPES
|
|
1698
|
+
Serializer type (``"create"`` | ``"update"`` | ``"read"`` | ``"detail"``).
|
|
1699
|
+
f_type : F_TYPES
|
|
1700
|
+
Field category (``"fields"`` | ``"optionals"`` | ``"customs"`` | ``"excludes"``).
|
|
1701
|
+
|
|
1702
|
+
Returns
|
|
1703
|
+
-------
|
|
1704
|
+
list
|
|
1705
|
+
Raw configuration list, or empty list if not configured.
|
|
1706
|
+
"""
|
|
1197
1707
|
schema_key = cls._SCHEMA_META_MAP.get(s_type)
|
|
1198
1708
|
if not schema_key:
|
|
1199
1709
|
return []
|
|
@@ -1248,13 +1758,13 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1248
1758
|
self.after_save(instance)
|
|
1249
1759
|
return instance
|
|
1250
1760
|
|
|
1251
|
-
async def create(self, payload: dict[str, Any]) -> models.Model:
|
|
1761
|
+
async def create(self, payload: dict[str, Any] | Schema) -> models.Model:
|
|
1252
1762
|
"""
|
|
1253
1763
|
Create a new model instance from the provided payload.
|
|
1254
1764
|
|
|
1255
1765
|
Parameters
|
|
1256
1766
|
----------
|
|
1257
|
-
payload : dict
|
|
1767
|
+
payload : dict | Schema
|
|
1258
1768
|
Input data.
|
|
1259
1769
|
|
|
1260
1770
|
Returns
|
|
@@ -1262,11 +1772,11 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1262
1772
|
models.Model
|
|
1263
1773
|
Created model instance.
|
|
1264
1774
|
"""
|
|
1265
|
-
instance: models.Model = self.model(**payload)
|
|
1775
|
+
instance: models.Model = self.model(**self._parse_payload(payload))
|
|
1266
1776
|
return await self.save(instance)
|
|
1267
1777
|
|
|
1268
1778
|
async def update(
|
|
1269
|
-
self, instance: models.Model, payload: dict[str, Any]
|
|
1779
|
+
self, instance: models.Model, payload: dict[str, Any] | Schema
|
|
1270
1780
|
) -> models.Model:
|
|
1271
1781
|
"""
|
|
1272
1782
|
Update an existing model instance with the provided payload.
|
|
@@ -1275,7 +1785,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1275
1785
|
----------
|
|
1276
1786
|
instance : models.Model
|
|
1277
1787
|
The model instance to update.
|
|
1278
|
-
payload : dict
|
|
1788
|
+
payload : dict | Schema
|
|
1279
1789
|
Input data.
|
|
1280
1790
|
|
|
1281
1791
|
Returns
|
|
@@ -1283,7 +1793,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1283
1793
|
models.Model
|
|
1284
1794
|
Updated model instance.
|
|
1285
1795
|
"""
|
|
1286
|
-
for attr, value in payload.items():
|
|
1796
|
+
for attr, value in self._parse_payload(payload).items():
|
|
1287
1797
|
setattr(instance, attr, value)
|
|
1288
1798
|
return await self.save(instance)
|
|
1289
1799
|
|