From e2566d71445260d0fec0d6a3681b0c54e4c80d5f Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Tue, 18 Nov 2025 21:35:33 +0100 Subject: [PATCH] Add `validation_alias` and `serialization_alias` properties to `ModelField` --- fastapi/_compat/v2.py | 20 +++++++++++++++++++- fastapi/dependencies/utils.py | 25 ++++++++++++++----------- fastapi/openapi/utils.py | 5 +++-- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 6a87b9ae9..53c9591d7 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -70,6 +70,18 @@ class ModelField: a = self.field_info.alias return a if a is not None else self.name + @property + def validation_alias(self) -> Union[str, None]: + va = self.field_info.validation_alias + if isinstance(va, str) and va: + return va + return None + + @property + def serialization_alias(self) -> Union[str, None]: + sa = self.field_info.serialization_alias + return sa or None + @property def required(self) -> bool: return self.field_info.is_required() @@ -183,12 +195,18 @@ def get_schema_from_model_field( override_mode: Union[Literal["validation"], None] = ( None if separate_input_output_schemas else "validation" ) + field_alias = ( + (field.validation_alias or field.alias) + if field.mode == "validation" + else (field.serialization_alias or field.alias) + ) + # This expects that GenerateJsonSchema was already used to generate the definitions json_schema = field_mapping[(field, override_mode or field.mode)] if "$ref" not in json_schema: # TODO remove when deprecating Pydantic v1 # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 - json_schema["title"] = field.field_info.title or field.alias.title().replace( + json_schema["title"] = field.field_info.title or field_alias.title().replace( "_", " " ) return json_schema diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4b69e39a1..e51b1f2da 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -722,7 +722,7 @@ def _validate_value_with_model_field( def _get_multidict_value( field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None ) -> Any: - alias = alias or field.alias + alias = alias or get_validation_alias(field) if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): value = values.getlist(alias) else: @@ -779,15 +779,13 @@ def request_params_to_args( field.field_info, "convert_underscores", default_convert_underscores ) if convert_underscores: - alias = ( - field.alias - if field.alias != field.name - else field.name.replace("_", "-") - ) + alias = get_validation_alias(field) + if alias == field.name: + alias = alias.replace("_", "-") value = _get_multidict_value(field, received_params, alias=alias) if value is not None: params_to_process[field.name] = value - processed_keys.add(alias or field.alias) + processed_keys.add(alias or get_validation_alias(field)) processed_keys.add(field.name) for key, value in received_params.items(): @@ -811,7 +809,7 @@ def request_params_to_args( assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( "Params must be subclasses of Param" ) - loc = (field_info.in_.value, field.alias) + loc = (field_info.in_.value, get_validation_alias(field)) v_, errors_ = _validate_value_with_model_field( field=field, value=value, values=values, loc=loc ) @@ -900,7 +898,7 @@ async def _extract_form_body( tg.start_soon(process_fn, sub_value.read) value = serialize_sequence_value(field=field, value=results) if value is not None: - values[field.alias] = value + values[get_validation_alias(field)] = value for key, value in received_body.items(): if key not in values: values[key] = value @@ -938,11 +936,11 @@ async def request_body_to_args( ) return {first_field.name: v_}, errors_ for field in body_fields: - loc = ("body", field.alias) + loc = ("body", get_validation_alias(field)) value: Optional[Any] = None if body_to_process is not None: try: - value = body_to_process.get(field.alias) + value = body_to_process.get(get_validation_alias(field)) # If the received body is a list, not a dict except AttributeError: errors.append(get_missing_field_error(loc)) @@ -1021,3 +1019,8 @@ def get_body_field( field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), ) return final_field + + +def get_validation_alias(field: ModelField) -> str: + va = getattr(field, "validation_alias", None) + return va or field.alias diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index dbc93d289..b401af97d 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -19,6 +19,7 @@ from fastapi.dependencies.utils import ( _get_flat_fields_from_params, get_flat_dependant, get_flat_params, + get_validation_alias, ) from fastapi.encoders import jsonable_encoder from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX @@ -132,7 +133,7 @@ def _get_openapi_operation_parameters( field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, ) - name = param.alias + name = get_validation_alias(param) convert_underscores = getattr( param.field_info, "convert_underscores", @@ -140,7 +141,7 @@ def _get_openapi_operation_parameters( ) if ( param_type == ParamTypes.header - and param.alias == param.name + and name == param.name and convert_underscores ): name = param.name.replace("_", "-")