Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: allow empty string aliases with AliasGenerator #8810

Merged
merged 1 commit into from Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 14 additions & 5 deletions pydantic/_internal/_generate_schema.py
Expand Up @@ -296,6 +296,15 @@ def push(self, for_type: type[Any]):
self._types_namespace_stack.pop()


def _get_first_non_null(a: Any, b: Any) -> Any:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def _get_first_non_null(a: Any, b: Any) -> Any:
def _get_first_not_none(a: Any, b: Any) -> Any:

not a big deal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, I don't think it would be crazy to just copy the if <a> is not None else everywhere, but I'm okay with this too.

"""Return the first argument if it is not None, otherwise return the second argument.

Use case: serialization_alias (argument a) and alias (argument b) are both defined, and serialization_alias is ''.
This function will return serialization_alias, which is the first argument, even though it is an empty string.
"""
return a if a is not None else b


class GenerateSchema:
"""Generate core schema for a Pydantic model, dataclass and types like `str`, `datetime`, ... ."""

Expand Down Expand Up @@ -997,17 +1006,17 @@ def _apply_alias_generator_to_field_info(

# if the priority is 1, then we set the aliases to the generated alias
if field_info.alias_priority == 1:
field_info.serialization_alias = serialization_alias or alias
field_info.validation_alias = validation_alias or alias
field_info.serialization_alias = _get_first_non_null(serialization_alias, alias)
field_info.validation_alias = _get_first_non_null(validation_alias, alias)
field_info.alias = alias

# if any of the aliases are not set, then we set them to the corresponding generated alias
if field_info.alias is None:
field_info.alias = alias
if field_info.serialization_alias is None:
field_info.serialization_alias = serialization_alias or alias
field_info.serialization_alias = _get_first_non_null(serialization_alias, alias)
if field_info.validation_alias is None:
field_info.validation_alias = validation_alias or alias
field_info.validation_alias = _get_first_non_null(validation_alias, alias)

@staticmethod
def _apply_alias_generator_to_computed_field_info(
Expand Down Expand Up @@ -1050,7 +1059,7 @@ def _apply_alias_generator_to_computed_field_info(
# note that we use the serialization_alias with priority over alias, as computed_field
# aliases are used for serialization only (not validation)
if computed_field_info.alias_priority == 1:
computed_field_info.alias = serialization_alias or alias
computed_field_info.alias = _get_first_non_null(serialization_alias, alias)

def _common_field_schema( # C901
self, name: str, field_info: FieldInfo, decorators: DecoratorInfos
Expand Down
32 changes: 32 additions & 0 deletions tests/test_aliases.py
Expand Up @@ -725,3 +725,35 @@ def area(self) -> int:

r = Rectangle(width_val_alias=10, height_val_alias=20)
assert r.model_dump(by_alias=True) == {'width_ser_alias': 10, 'height_ser_alias': 20, 'area_ser_alias': 200}


empty_str_alias_generator = AliasGenerator(
validation_alias=lambda x: '', alias=lambda x: f'{x}_alias', serialization_alias=lambda x: ''
)


def test_alias_gen_with_empty_string() -> None:
class Model(BaseModel):
a: str

model_config = ConfigDict(alias_generator=empty_str_alias_generator)

assert Model.model_fields['a'].validation_alias == ''
assert Model.model_fields['a'].serialization_alias == ''
assert Model.model_fields['a'].alias == 'a_alias'


def test_alias_gen_with_empty_string_and_computed_field() -> None:
class Model(BaseModel):
model_config = ConfigDict(alias_generator=empty_str_alias_generator)

a: str

@computed_field
def b(self) -> str:
return self.a

assert Model.model_fields['a'].validation_alias == ''
assert Model.model_fields['a'].serialization_alias == ''
assert Model.model_fields['a'].alias == 'a_alias'
assert Model.model_computed_fields['b'].alias == ''