77 Iterable ,
88 List ,
99 Optional ,
10+ Set ,
1011 Tuple ,
1112 Type ,
1213 TypeVar ,
1314 Union ,
1415)
1516
1617import pydantic
17- from pydantic import BaseConfig
18+ from pydantic import BaseConfig , root_validator , validator
1819from pydantic import BaseModel as PydanticBaseModel
19- from pydantic .fields import FieldInfo , ModelField
20+ from pydantic .class_validators import ROOT_VALIDATOR_CONFIG_KEY , VALIDATOR_CONFIG_KEY
21+ from pydantic .fields import FieldInfo , ModelField , Validator
2022
2123from fastapi_jsonapi .data_typing import TypeSchema
2224from fastapi_jsonapi .schema import (
@@ -64,13 +66,13 @@ class BuiltSchemasDTO:
6466 list_response_schema : Type [JSONAPIResultListSchema ]
6567
6668
67- FieldValidator = Optional [ Any ]
69+ FieldValidators = Dict [ str , Callable ]
6870
6971
7072@dataclass (frozen = True )
7173class SchemasInfoDTO :
7274 # id field
73- resource_id_field : Tuple [Type , FieldInfo , Callable ]
75+ resource_id_field : Tuple [Type , FieldInfo , Callable , FieldValidators ]
7476 # pre-built attributes
7577 attributes_schema : Type [BaseModel ]
7678 # relationships
@@ -236,7 +238,7 @@ def _get_info_from_schema_for_building(
236238 relationships_schema_fields = {}
237239 included_schemas : List [Tuple [str , BaseModel , str ]] = []
238240 has_required_relationship = False
239- resource_id_field = (str , Field (None ), None )
241+ resource_id_field = (str , Field (None ), None , {} )
240242
241243 for name , field in (schema .__fields__ or {}).items ():
242244 if isinstance (field .field_info .extra .get ("relationship" ), RelationshipInfo ):
@@ -263,12 +265,15 @@ def _get_info_from_schema_for_building(
263265 # works both for to-one and to-many
264266 included_schemas .append ((name , field .type_ , relationship .resource_type ))
265267 elif name == "id" :
268+ id_validators = self ._extract_field_validators (schema , target_field_name = "id" )
269+ resource_id_field = (* (resource_id_field [:- 1 ]), id_validators )
270+
266271 if not field .field_info .extra .get ("client_can_set_id" ):
267272 continue
268273
269274 # todo: support for union types?
270275 # support custom cast func
271- resource_id_field = (str , Field (** field .field_info .extra ), field .outer_type_ )
276+ resource_id_field = (str , Field (** field .field_info .extra ), field .outer_type_ , id_validators )
272277 else :
273278 attributes_schema_fields [name ] = (field .outer_type_ , field .field_info )
274279
@@ -279,6 +284,7 @@ class ConfigOrmMode(BaseConfig):
279284 f"{ base_name } AttributesJSONAPI" ,
280285 ** attributes_schema_fields ,
281286 __config__ = ConfigOrmMode ,
287+ __validators__ = self ._extract_validators (schema , exclude_for_field_names = {"id" }),
282288 )
283289
284290 relationships_schema = pydantic .create_model (
@@ -346,14 +352,119 @@ def create_relationship_data_schema(
346352 self .relationship_schema_cache [cache_key ] = relationship_data_schema
347353 return relationship_data_schema
348354
355+ def _is_target_validator (self , attr_name : str , value : Any , validator_config_key : str ) -> bool :
356+ """
357+ True if passed object is validator of type identified by "validator_config_key" arg
358+
359+ :param attr_name:
360+ :param value:
361+ :param validator_config_key: Choice field, available options are pydantic consts
362+ VALIDATOR_CONFIG_KEY, ROOT_VALIDATOR_CONFIG_KEY
363+ """
364+ return (
365+ # also with private items
366+ not attr_name .startswith ("__" )
367+ and getattr (value , validator_config_key , None )
368+ )
369+
370+ def _unpack_validators (self , model : Type [BaseModel ], validator_config_key : str ) -> Dict [str , Validator ]:
371+ """
372+ Selects all validators from model attrs and unpack them from class methods
373+
374+ :param model: Type[BaseModel]
375+ :param validator_config_key: Choice field, available options are pydantic consts
376+ VALIDATOR_CONFIG_KEY, ROOT_VALIDATOR_CONFIG_KEY
377+ """
378+ root_validator_class_methods = {
379+ # validators only
380+ attr_name : value
381+ for attr_name , value in model .__dict__ .items ()
382+ if self ._is_target_validator (attr_name , value , validator_config_key )
383+ }
384+
385+ return {
386+ validator_name : getattr (validator_method , validator_config_key )
387+ for validator_name , validator_method in root_validator_class_methods .items ()
388+ }
389+
390+ def _extract_root_validators (self , model : Type [BaseModel ]) -> Dict [str , Callable ]:
391+ validators = {}
392+
393+ unpacked_validators = self ._unpack_validators (model , ROOT_VALIDATOR_CONFIG_KEY )
394+ for validator_name , validator_instance in unpacked_validators .items ():
395+ validators [validator_name ] = root_validator (
396+ pre = validator_instance .pre ,
397+ skip_on_failure = validator_instance .skip_on_failure ,
398+ allow_reuse = True ,
399+ )(validator_instance .func )
400+
401+ return validators
402+
403+ def _extract_field_validators (
404+ self ,
405+ model : Type [BaseModel ],
406+ target_field_name : str = None ,
407+ exclude_for_field_names : Set [str ] = None ,
408+ ) -> Dict [str , Callable ]:
409+ """
410+ :param model: Type[BaseModel]
411+ :param target_field_name: Name of field for which validators will be returned.
412+ If not set the function will return validators for all fields.
413+ """
414+ validators = {}
415+ validator_origin_param_keys = ("pre" , "each_item" , "always" , "check_fields" )
416+
417+ unpacked_validators = self ._unpack_validators (model , VALIDATOR_CONFIG_KEY )
418+ for validator_name , (field_names , validator_instance ) in unpacked_validators .items ():
419+ if target_field_name and target_field_name not in field_names :
420+ continue
421+ elif target_field_name :
422+ field_names = [target_field_name ] # noqa: PLW2901
423+
424+ if exclude_for_field_names :
425+ field_names = [ # noqa: PLW2901
426+ # filter names
427+ field_name
428+ for field_name in field_names
429+ if field_name not in exclude_for_field_names
430+ ]
431+
432+ if not field_names :
433+ continue
434+
435+ validators [validator_name ] = validator (
436+ * field_names ,
437+ allow_reuse = True ,
438+ ** {
439+ # copy origin params
440+ param_name : getattr (validator_instance , param_name )
441+ for param_name in validator_origin_param_keys
442+ },
443+ )(validator_instance .func )
444+
445+ return validators
446+
447+ def _extract_validators (
448+ self ,
449+ model : Type [BaseModel ],
450+ exclude_for_field_names : Set [str ] = None ,
451+ ) -> Dict [str , Callable ]:
452+ return {
453+ ** self ._extract_field_validators (
454+ model ,
455+ exclude_for_field_names = exclude_for_field_names ,
456+ ),
457+ ** self ._extract_root_validators (model ),
458+ }
459+
349460 def _build_jsonapi_object (
350461 self ,
351462 base_name : str ,
352463 resource_type : str ,
353464 attributes_schema : Type [TypeSchema ],
354465 relationships_schema : Type [TypeSchema ],
355466 includes ,
356- resource_id_field : Tuple [Type , FieldInfo , Callable ],
467+ resource_id_field : Tuple [Type , FieldInfo , Callable , FieldValidators ],
357468 model_base : Type [JSONAPIObjectSchemaType ] = JSONAPIObjectSchema ,
358469 use_schema_cache : bool = True ,
359470 relationships_required : bool = False ,
@@ -362,7 +473,7 @@ def _build_jsonapi_object(
362473 if use_schema_cache and base_name in self .base_jsonapi_object_schemas_cache :
363474 return self .base_jsonapi_object_schemas_cache [base_name ]
364475
365- field_type , field_info , id_cast_func = resource_id_field
476+ field_type , field_info , id_cast_func , id_validators = resource_id_field
366477
367478 id_field_kw = {
368479 ** field_info .extra ,
@@ -383,6 +494,7 @@ def _build_jsonapi_object(
383494 f"{ base_name } ObjectJSONAPI" ,
384495 ** object_jsonapi_schema_fields ,
385496 type = (str , Field (default = resource_type or self ._resource_type , description = "Resource type" )),
497+ __validators__ = id_validators ,
386498 __base__ = model_base ,
387499 )
388500
0 commit comments