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.
@@ -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
- # Subclasses provide implementation.
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
- # Subclasses provide implementation.
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
- # Optional in subclasses. Default to no explicit relation serializers.
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
- # Optional in subclasses. Default to no relations as ID.
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
- """Return True if field appears in the given category for s_type."""
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
- - (name, py_type, default)
343
- - (name, py_type) -> default Ellipsis (required)
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
- """Return optional field specs normalized to (name, type, None)."""
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
- """Return excluded field names for the serializer type."""
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
- """Return explicit declared field names for the serializer type (excludes inline customs)."""
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
- """True if field is declared as a custom input in create or update."""
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
- """True if field is declared as optional in create or update."""
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
- """Check if field is a reverse relation (M2M, reverse FK, reverse O2O)."""
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
- """Check if field is a forward relation (FK, O2O)."""
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
- """Emit warning for reverse relations without explicit serializer mapping."""
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
- Handles In/Patch/Out/Related.
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
- return create_schema(
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
- return create_schema(
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
- return create_schema(
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
- """Generate read (Out) schema."""
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
- """Generate detail (single object Out) schema."""
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
- """Generate create (In) schema."""
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
- """Generate update (Patch) schema."""
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
- """Generate related (nested) schema."""
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
- """Return relation fields to serialize as IDs instead of nested objects."""
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
- """Return the model class itself."""
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
- """Internal accessor for raw configuration lists from Meta schemas."""
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