@@ -132,6 +132,65 @@ def _normalize_validation_message(
132132 return NormalizedValidationMessage (normalized )
133133
134134
135+ def _normalize_pydantic_error (
136+ error : Mapping [str , object ],
137+ field_def : FieldMetaInfo | None = None ,
138+ * ,
139+ excel_codec : type [ExcelFieldCodec ] | None = None ,
140+ ) -> NormalizedValidationMessage :
141+ normalized_message = _normalize_constraint_error (error , field_def , excel_codec = excel_codec )
142+ if normalized_message is not None :
143+ return normalized_message
144+ return _normalize_validation_message (error .get ('msg' , '' ), field_def , excel_codec = excel_codec )
145+
146+
147+ def _normalize_constraint_error (
148+ error : Mapping [str , object ],
149+ field_def : FieldMetaInfo | None ,
150+ * ,
151+ excel_codec : type [ExcelFieldCodec ] | None = None ,
152+ ) -> NormalizedValidationMessage | None :
153+ if field_def is None :
154+ return None
155+
156+ error_type = error .get ('type' )
157+ ctx = error .get ('ctx' )
158+ if not isinstance (ctx , Mapping ):
159+ return None
160+
161+ ctx = cast (Mapping [object , object ], ctx )
162+ field_type = ctx .get ('field_type' )
163+ constraints = field_def .constraints
164+ if error_type == 'too_short' and field_type == 'List' :
165+ min_items = _int_from_context (ctx , 'min_length' ) or constraints .min_items
166+ if min_items is not None :
167+ return NormalizedValidationMessage .from_key (MessageKey .MIN_ITEMS_REQUIRED , min_items = min_items )
168+
169+ if error_type == 'too_long' and field_type == 'List' :
170+ max_items = _int_from_context (ctx , 'max_length' ) or constraints .max_items
171+ if max_items is not None :
172+ return NormalizedValidationMessage .from_key (MessageKey .MAX_ITEMS_ALLOWED , max_items = max_items )
173+
174+ if error_type == 'string_too_short' :
175+ min_length = _int_from_context (ctx , 'min_length' ) or constraints .min_length
176+ if min_length is not None :
177+ return NormalizedValidationMessage .from_key (MessageKey .MIN_LENGTH_CHARACTERS , min_length = min_length )
178+
179+ if error_type == 'string_too_long' :
180+ max_length = _int_from_context (ctx , 'max_length' ) or constraints .max_length
181+ if max_length is not None :
182+ return NormalizedValidationMessage .from_key (MessageKey .MAX_LENGTH_CHARACTERS , max_length = max_length )
183+
184+ return _normalize_constraint_message (str (error .get ('msg' , '' )), field_def , excel_codec = excel_codec )
185+
186+
187+ def _int_from_context (ctx : Mapping [object , object ], key : str ) -> int | None :
188+ value = ctx .get (key )
189+ if isinstance (value , int ):
190+ return value
191+ return None
192+
193+
135194def _normalize_constraint_message (
136195 message : str ,
137196 field_def : FieldMetaInfo | None ,
@@ -293,10 +352,50 @@ def fields(self) -> Iterable[PydanticFieldAdapter]:
293352 def field (self , name : str ) -> PydanticFieldAdapter :
294353 return PydanticFieldAdapter (name = name , raw_field = self .model .model_fields [name ])
295354
355+ def field_for_validation_location (self , location : str ) -> PydanticFieldAdapter | None :
356+ field_name = self ._field_name_for_validation_location (location )
357+ if field_name is None :
358+ return None
359+ return self .field (field_name )
360+
361+ def _field_name_for_validation_location (self , location : str ) -> str | None :
362+ if location in self .model .model_fields :
363+ return location
364+
365+ for name , field_info in self .model .model_fields .items ():
366+ if location in _validation_locations_for_field (field_info ):
367+ return name
368+
369+ return None
370+
296371 def field_names (self ) -> list [str ]:
297372 return list (self .model .model_fields .keys ())
298373
299374
375+ def _validation_locations_for_field (field_info : FieldInfo ) -> set [str ]:
376+ locations : set [str ] = set ()
377+ if isinstance (field_info .alias , str ):
378+ locations .add (field_info .alias )
379+ _collect_validation_alias_locations (field_info .validation_alias , locations )
380+ return locations
381+
382+
383+ def _collect_validation_alias_locations (alias : object , locations : set [str ]) -> None :
384+ if isinstance (alias , str ):
385+ locations .add (alias )
386+ return
387+
388+ choices = getattr (alias , 'choices' , None )
389+ if isinstance (choices , (list , tuple )):
390+ for choice in cast (Iterable [object ], choices ):
391+ _collect_validation_alias_locations (choice , locations )
392+ return
393+
394+ path = getattr (alias , 'path' , None )
395+ if isinstance (path , (list , tuple )) and path and isinstance (path [0 ], str ):
396+ locations .add (path [0 ])
397+
398+
300399def extract_pydantic_model (
301400 model : type [BaseModel ] | None ,
302401) -> list [FieldMetaInfo ]:
@@ -383,7 +482,7 @@ def _model_validate[ModelT: BaseModel](
383482 failed_fields : set [str ],
384483) -> ModelT | list [ExcelCellError | ExcelRowError ]:
385484 try :
386- return model .model_validate (data )
485+ return model .model_validate (data , by_alias = False , by_name = True )
387486 except ValidationError as exc :
388487 return _map_validation_error (exc , model_adapter , failed_fields )
389488
@@ -397,21 +496,26 @@ def _map_validation_error(
397496 for error in exc .errors ():
398497 loc = error .get ('loc' , ())
399498 if not loc :
400- normalized = _normalize_validation_message ( str ( error [ 'msg' ]) )
499+ normalized = _normalize_pydantic_error ( error )
401500 mapped .append (_build_row_error (normalized ))
402501 continue
403502
404503 field_name = loc [0 ]
405504 if not isinstance (field_name , str ):
406- normalized = _normalize_validation_message ( str ( error [ 'msg' ]) )
505+ normalized = _normalize_pydantic_error ( error )
407506 mapped .append (_build_row_error (normalized ))
408507 continue
409508 if field_name in failed_fields :
410509 continue
411510
412- field_adapter = model_adapter .field (field_name )
413- normalized = _normalize_validation_message (
414- str (error ['msg' ]),
511+ field_adapter = model_adapter .field_for_validation_location (field_name )
512+ if field_adapter is None :
513+ normalized = _normalize_pydantic_error (error )
514+ mapped .append (_build_row_error (normalized ))
515+ continue
516+
517+ normalized = _normalize_pydantic_error (
518+ error ,
415519 field_adapter .declared_metadata ,
416520 excel_codec = field_adapter .excel_codec ,
417521 )
0 commit comments