Repository: pydantic/pydantic

Issue:
V2: alias generator
support both camel case and pascal case. Related to #3464.

Python function that modifies core schema.


Discussion:

Resolving patch:
diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py
--- a/pydantic/_internal/_model_construction.py
+++ b/pydantic/_internal/_model_construction.py
@@ -159,6 +159,7 @@ def complete_model_class(
     types_namespace = {**(types_namespace or {}), cls.__name__: PydanticForwardRef(self_schema, cls)}
     try:
         fields, class_vars = collect_fields(cls, bases, types_namespace, typevars_map=typevars_map)
+        apply_alias_generator(cls.model_config, fields)
         # this schema construction has to go here
         # since in some recursive generics it can raise a PydanticUndefinedAnnotation error
         inner_schema = model_fields_schema(
@@ -303,3 +304,17 @@ def __getattr__(self, item: str) -> None:
         # raise an AttributeError if `item` doesn't exist
         getattr(SchemaValidator, item)
         raise PydanticUserError(self._error_message)
+
+
+def apply_alias_generator(config: ConfigDict, fields: dict[str, FieldInfo]) -> None:
+    alias_generator = config['alias_generator']
+    if alias_generator is None:
+        return
+
+    for name, field_info in fields.items():
+        if field_info.alias_priority is None or field_info.alias_priority <= 1:
+            alias = alias_generator(name)
+            if not isinstance(alias, str):
+                raise TypeError(f'alias_generator {alias_generator} must return str, not {alias.__class__}')
+            field_info.alias = alias
+            field_info.alias_priority = 1
diff --git a/pydantic/fields.py b/pydantic/fields.py
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -80,7 +80,7 @@ def __init__(self, **kwargs: Any) -> None:
             raise ValueError('cannot specify both default and default_factory')
 
         self.alias = kwargs.get('alias')
-        self.alias_priority = kwargs.get('alias_priority', 2 if self.alias is not None else None)
+        self.alias_priority = kwargs.get('alias_priority') or 2 if self.alias is not None else None
         self.title = kwargs.get('title')
         self.description = kwargs.get('description')
         self.examples = kwargs.get('examples')
@@ -282,6 +282,10 @@ def Field(
     *,
     default_factory: typing.Callable[[], Any] | None = None,
     alias: str = None,
+    # TODO:
+    #  Alternative 1: we could drop alias_priority and tell people to manually override aliases in child classes
+    #  Alternative 2: we could add a new argument `override_with_alias_generator=True` equivalent to `alias_priority=1`
+    alias_priority: int = None,
     title: str = None,
     description: str = None,
     examples: list[Any] = None,
@@ -360,6 +364,7 @@ def Field(
         default,
         default_factory=default_factory,
         alias=alias,
+        alias_priority=alias_priority,
         title=title,
         description=description,
         examples=examples,


Test patch:
diff --git a/tests/test_aliases.py b/tests/test_aliases.py
--- a/tests/test_aliases.py
+++ b/tests/test_aliases.py
@@ -2,12 +2,12 @@
 from typing import Any, ContextManager, List, Optional
 
 import pytest
+from dirty_equals import IsStr
 
 from pydantic import BaseModel, ConfigDict, Extra, ValidationError
 from pydantic.fields import Field
 
 
-@pytest.mark.xfail(reason='working on V2')
 def test_alias_generator():
     def to_camel(string: str):
         return ''.join(x.capitalize() for x in string.split('_'))
@@ -24,27 +24,6 @@ class MyModel(BaseModel):
     assert v.model_dump(by_alias=True) == data
 
 
-@pytest.mark.xfail(reason='working on V2')
-def test_alias_generator_with_field_schema():
-    def to_upper_case(string: str):
-        return string.upper()
-
-    class MyModel(BaseModel):
-        model_config = ConfigDict(alias_generator=to_upper_case)
-        my_shiny_field: Any  # Alias from Config.fields will be used
-        foo_bar: str  # Alias from Config.fields will be used
-        baz_bar: str  # Alias will be generated
-        another_field: str  # Alias will be generated
-
-        class Config:
-            fields = {'my_shiny_field': 'MY_FIELD', 'foo_bar': {'alias': 'FOO'}, 'another_field': {'not_alias': 'a'}}
-
-    data = {'MY_FIELD': ['a'], 'FOO': 'bar', 'BAZ_BAR': 'ok', 'ANOTHER_FIELD': '...'}
-    m = MyModel(**data)
-    assert m.model_dump(by_alias=True) == data
-
-
-@pytest.mark.xfail(reason='working on V2')
 def test_alias_generator_wrong_type_error():
     def return_bytes(string):
         return b'not a string'
@@ -55,7 +34,7 @@ class MyModel(BaseModel):
             model_config = ConfigDict(alias_generator=return_bytes)
             bar: Any
 
-    assert str(e.value) == "Config.alias_generator must return str, not <class 'bytes'>"
+    assert str(e.value) == IsStr(regex="alias_generator <function .*> must return str, not <class 'bytes'>")
 
 
 @pytest.mark.xfail(reason='working on V2')
@@ -127,7 +106,51 @@ class Model(BaseModel):
     ]
 
 
-@pytest.mark.xfail(reason='working on V2')
+def test_alias_override_behavior():
+    class Parent(BaseModel):
+        # Use `gt` to demonstrate that using `Field` to override an alias does not preserve other attributes
+        x: int = Field(alias='x1', gt=0)
+
+    class Child(Parent):
+        x = Field(..., alias='x2')
+        y: int = Field(..., alias='y2')
+
+    assert Parent.model_fields['x'].alias == 'x1'
+    assert Child.model_fields['x'].alias == 'x2'
+    assert Child.model_fields['y'].alias == 'y2'
+
+    Parent(x1=1)
+    with pytest.raises(ValidationError) as exc_info:
+        Parent(x1=-1)
+    assert exc_info.value.errors() == [
+        {'ctx': {'gt': 0}, 'input': -1, 'loc': ('x',), 'msg': 'Input should be greater than 0', 'type': 'greater_than'}
+    ]
+
+    Child(x2=1, y2=2)
+
+    # Check the gt=0 is not preserved from Parent
+    Child(x2=-1, y2=2)
+
+    # Check the alias from Parent cannot be used
+    with pytest.raises(ValidationError) as exc_info:
+        Child(x1=1, y2=2)
+    assert exc_info.value.errors() == [
+        {'input': {'x1': 1, 'y2': 2}, 'loc': ('x',), 'msg': 'Field required', 'type': 'missing'}
+    ]
+
+    # Check the type hint from Parent _is_ preserved
+    with pytest.raises(ValidationError) as exc_info:
+        Child(x2='a', y2=2)
+    assert exc_info.value.errors() == [
+        {
+            'input': 'a',
+            'loc': ('x',),
+            'msg': 'Input should be a valid integer, unable to parse string as an ' 'integer',
+            'type': 'int_parsing',
+        }
+    ]
+
+
 def test_alias_generator_parent():
     class Parent(BaseModel):
         model_config = ConfigDict(populate_by_name=True, alias_generator=lambda f_name: f_name + '1')
@@ -141,7 +164,6 @@ class Child(Parent):
     assert Child.model_fields['x'].alias == 'x2'
 
 
-@pytest.mark.xfail(reason='working on V2')
 def test_alias_generator_on_parent():
     class Parent(BaseModel):
         model_config = ConfigDict(alias_generator=lambda x: x.upper())
@@ -159,7 +181,6 @@ class Child(Parent):
     assert Child.model_fields['z'].alias == 'Z'
 
 
-@pytest.mark.xfail(reason='working on V2')
 def test_alias_generator_on_child():
     class Parent(BaseModel):
         x: bool = Field(..., alias='abc')
@@ -171,17 +192,16 @@ class Child(Parent):
         y: str
         z: str
 
-    assert [f.alias for f in Parent.model_fields.values()] == ['abc', 'y']
+    assert [f.alias for f in Parent.model_fields.values()] == ['abc', None]
     assert [f.alias for f in Child.model_fields.values()] == ['abc', 'Y', 'Z']
 
 
-@pytest.mark.xfail(reason='working on V2')
 def test_low_priority_alias():
-    # TODO: alias_priority has been removed from `Field`. Should we re-add it?
-    #   Is there something new that can be used to replicate this functionality?
-    #   See discussion in https://github.com/pydantic/pydantic/pull/5181/files#r1137618854
-    #   Either way, if we don't re-add it to `Field`, remember to update the migration guide
+    # TODO:
+    #  Alternative 1: we could drop alias_priority and tell people to manually override aliases in child classes
+    #  Alternative 2: we could add a new argument `override_with_alias_generator=True` equivalent to `alias_priority=1`
     class Parent(BaseModel):
+        w: bool = Field(..., alias='w_')
         x: bool = Field(..., alias='abc', alias_priority=1)
         y: str
 
@@ -191,90 +211,8 @@ class Child(Parent):
         y: str
         z: str
 
-    assert [f.alias for f in Parent.model_fields.values()] == ['abc', 'y']
-    assert [f.alias for f in Child.model_fields.values()] == ['X', 'Y', 'Z']
-
-
-@pytest.mark.xfail(reason='working on V2')
-def test_low_priority_alias_config():
-    class Parent(BaseModel):
-        x: bool
-        y: str
-
-        class Config:
-            fields = {'x': dict(alias='abc', alias_priority=1)}
-
-    class Child(Parent):
-        y: str
-        z: str
-
-        class Config:
-            @staticmethod
-            def alias_generator(x):
-                return x.upper()
-
-    assert [f.alias for f in Parent.model_fields.values()] == ['abc', 'y']
-    assert [f.alias for f in Child.model_fields.values()] == ['X', 'Y', 'Z']
-
-
-@pytest.mark.xfail(reason='working on V2')
-def test_field_vs_config():
-    class Model(BaseModel):
-        x: str = Field(..., alias='x_on_field')
-        y: str
-        z: str
-
-        class Config:
-            fields = {'x': dict(alias='x_on_config'), 'y': dict(alias='y_on_config')}
-
-    assert [f.alias for f in Model.model_fields.values()] == ['x_on_field', 'y_on_config', 'z']
-
-
-@pytest.mark.xfail(reason='working on V2')
-def test_alias_priority():
-    class Parent(BaseModel):
-        a: str = Field(..., alias='a_field_parent')
-        b: str = Field(..., alias='b_field_parent')
-        c: str = Field(..., alias='c_field_parent')
-        d: str
-        e: str
-
-        class Config:
-            fields = {
-                'a': dict(alias='a_config_parent'),
-                'c': dict(alias='c_config_parent'),
-                'd': dict(alias='d_config_parent'),
-            }
-
-            @staticmethod
-            def alias_generator(x):
-                return f'{x}_generator_parent'
-
-    class Child(Parent):
-        a: str = Field(..., alias='a_field_child')
-
-        class Config:
-            fields = {'a': dict(alias='a_config_child'), 'b': dict(alias='b_config_child')}
-
-            @staticmethod
-            def alias_generator(x):
-                return f'{x}_generator_child'
-
-    # debug([f.alias for f in Parent.model_fields.values()], [f.alias for f in Child.model_fields.values()])
-    assert [f.alias for f in Parent.model_fields.values()] == [
-        'a_field_parent',
-        'b_field_parent',
-        'c_field_parent',
-        'd_config_parent',
-        'e_generator_parent',
-    ]
-    assert [f.alias for f in Child.model_fields.values()] == [
-        'a_field_child',
-        'b_config_child',
-        'c_field_parent',
-        'd_config_parent',
-        'e_generator_child',
-    ]
+    assert [f.alias for f in Parent.model_fields.values()] == ['w_', 'abc', None]
+    assert [f.alias for f in Child.model_fields.values()] == ['w_', 'X', 'Y', 'Z']
 
 
 def test_empty_string_alias():
diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py
--- a/tests/test_json_schema.py
+++ b/tests/test_json_schema.py
@@ -157,7 +157,6 @@ class ApplePie(BaseModel):
     assert '#/$defs/KeyLimePie' not in json_schema
 
 
-@pytest.mark.xfail(reason='working on V2 - alias generator')
 def test_by_alias_generator():
     class ApplePie(BaseModel):
         model_config = ConfigDict(alias_generator=lambda x: x.upper())