Repository: pydantic/pydantic
Pull request: change alias priority logic
## Change Summary

This changes the priority of aliases to fix #1177, however it introduces a potential breaking change.

For example:

https://github.com/samuelcolvin/pydantic/blob/80152f337191ba7539e077c02a236bb713fdddd2/tests/test_aliases.py#L189-L204

before `alias_generator` on the child would take taken priority. While it's arguable whether this is correct and we could allow `alias_generator` to return a tuple `(<alias>, <alias priority>)` to allow customisation, I'm aware this is a breaking change.

However I'm not sure how else to fix the issue in a cleaner way. @tiangolo, @dmontagu, anyone else involved - do you think we should:
* merge this with a "potential breaking change" warning
* wait to merge this until v2
* provide some other fix for #1177 

?

## Checklist

* [x] Unit tests for the changes exist
* [ ] Tests pass on CI and coverage remains at 100%
* [ ] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added

Problem statement:
alias_generator used instead of field's alias on inheritance
# Bug

When `alias` is specified on a field for a class where `alias_generator` is also defined, `alias` is respected for that class but not any derived classes.

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```python
             pydantic version: 1.3
            pydantic compiled: False
                 install path: /<snip>/site-packages/pydantic
               python version: 3.7.6 (default, Dec 30 2019, 19:38:28)  [Clang 11.0.0 (clang-1100.0.33.16)]
                     platform: Darwin-18.7.0-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions', 'email-validator']
```

## Example code

```python
from pydantic import BaseModel, Field


class MyBaseModel(BaseModel):
    class Config:
        alias_generator = lambda x: x.upper()

class MyModel(MyBaseModel):
    my_attribute: bool = Field(..., alias='myATTRIBUTE')

class MyOtherModel(MyModel):
    pass
```

## Examples


```py
>>> MyModel()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<snip>/site-packages/pydantic/main.py", line 274, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for MyModel
myATTRIBUTE
  field required (type=value_error.missing)
>>> MyOtherModel()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<snip>/site-packages/pydantic/main.py", line 274, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for MyOtherModel
MY_ATTRIBUTE
  field required (type=value_error.missing)
```

Notice that `MyModel` requires `myATTRIBUTE`, as expected, but `MyOtherModel` requires `MY_ATTRIBUTE`, which is from the `alias_generator`

Hints from issue discussion:

Commit message: remove debugs

Patch to review:
diff --git a/changes/1177-samuelcolvin.md b/changes/1177-samuelcolvin.md
new file mode 100644
index 00000000000..02972a0d4b6
--- /dev/null
+++ b/changes/1177-samuelcolvin.md
@@ -0,0 +1 @@
+`alias` takes priority over `alias_generator` on sub-classes
diff --git a/pydantic/fields.py b/pydantic/fields.py
index c064dfbdcc6..c9818dd9341 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -18,7 +18,6 @@
     Type,
     TypeVar,
     Union,
-    cast,
 )
 
 from . import errors as errors_
@@ -60,6 +59,7 @@ class FieldInfo(Representation):
     __slots__ = (
         'default',
         'alias',
+        'alias_priority',
         'title',
         'description',
         'const',
@@ -79,6 +79,7 @@ class FieldInfo(Representation):
     def __init__(self, default: Any, **kwargs: Any) -> None:
         self.default = default
         self.alias = kwargs.pop('alias', None)
+        self.alias_priority = kwargs.pop('alias_priority', 2 if self.alias else None)
         self.title = kwargs.pop('title', None)
         self.description = kwargs.pop('description', None)
         self.const = kwargs.pop('const', None)
@@ -288,9 +289,12 @@ def set_config(self, config: Type['BaseConfig']) -> None:
         self.model_config = config
         info_from_config = config.get_field_info(self.name)
         config.prepare_field(self)
-        if info_from_config:
-            self.field_info.alias = info_from_config.get('alias') or self.field_info.alias or self.name
-            self.alias = cast(str, self.field_info.alias)
+        new_alias = info_from_config.get('alias')
+        new_alias_priority = info_from_config.get('alias_priority') or 0
+        if new_alias and new_alias_priority >= (self.field_info.alias_priority or 0):
+            self.field_info.alias = new_alias
+            self.field_info.alias_priority = new_alias_priority
+            self.alias = new_alias
 
     @property
     def alt_alias(self) -> bool:
diff --git a/pydantic/main.py b/pydantic/main.py
index 5372f5c67cd..943958a5324 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -73,14 +73,22 @@ class BaseConfig:
 
     @classmethod
     def get_field_info(cls, name: str) -> Dict[str, Any]:
-        field_info = cls.fields.get(name) or {}
-        if isinstance(field_info, str):
-            field_info = {'alias': field_info}
-        elif cls.alias_generator and 'alias' not in field_info:
+        fields_value = cls.fields.get(name)
+
+        if isinstance(fields_value, str):
+            field_info: Dict[str, Any] = {'alias': fields_value, 'alias_priority': 0}
+        elif isinstance(fields_value, dict):
+            field_info = fields_value
+        else:
+            field_info = {}
+
+        if 'alias' in field_info:
+            field_info['alias_priority'] = 2
+        elif cls.alias_generator:
             alias = cls.alias_generator(name)
             if not isinstance(alias, str):
                 raise TypeError(f'Config.alias_generator must return str, not {type(alias)}')
-            field_info['alias'] = alias
+            field_info.update(alias=alias, alias_priority=1)
         return field_info
 
     @classmethod
diff --git a/tests/test_aliases.py b/tests/test_aliases.py
new file mode 100644
index 00000000000..4831d12d078
--- /dev/null
+++ b/tests/test_aliases.py
@@ -0,0 +1,204 @@
+import re
+from typing import Optional
+
+import pytest
+
+from pydantic import BaseConfig, BaseModel, Extra, ValidationError
+from pydantic.fields import Field
+
+
+def test_infer_alias():
+    class Model(BaseModel):
+        a = 'foobar'
+
+        class Config:
+            fields = {'a': '_a'}
+
+    assert Model(_a='different').a == 'different'
+    assert repr(Model.__fields__['a']) == (
+        "ModelField(name='a', type=str, required=False, default='foobar', alias='_a')"
+    )
+
+
+def test_alias_error():
+    class Model(BaseModel):
+        a = 123
+
+        class Config:
+            fields = {'a': '_a'}
+
+    assert Model(_a='123').a == 123
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(_a='foo')
+    assert exc_info.value.errors() == [
+        {'loc': ('_a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
+    ]
+
+
+def test_annotation_config():
+    class Model(BaseModel):
+        b: float
+        a: int = 10
+        _c: str
+
+        class Config:
+            fields = {'b': 'foobar'}
+
+    assert list(Model.__fields__.keys()) == ['b', 'a']
+    assert [f.alias for f in Model.__fields__.values()] == ['foobar', 'a']
+    assert Model(foobar='123').b == 123.0
+
+
+def test_alias_camel_case():
+    class Model(BaseModel):
+        one_thing: int
+        another_thing: int
+
+        class Config(BaseConfig):
+            @classmethod
+            def get_field_info(cls, name):
+                field_config = super().get_field_info(name) or {}
+                if 'alias' not in field_config:
+                    field_config['alias'] = re.sub(r'(?:^|_)([a-z])', lambda m: m.group(1).upper(), name)
+                return field_config
+
+    v = Model(**{'OneThing': 123, 'AnotherThing': '321'})
+    assert v.one_thing == 123
+    assert v.another_thing == 321
+    assert v == {'one_thing': 123, 'another_thing': 321}
+
+
+def test_get_field_info_inherit():
+    class ModelOne(BaseModel):
+        class Config(BaseConfig):
+            @classmethod
+            def get_field_info(cls, name):
+                field_config = super().get_field_info(name) or {}
+                if 'alias' not in field_config:
+                    field_config['alias'] = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
+                return field_config
+
+    class ModelTwo(ModelOne):
+        one_thing: int
+        another_thing: int
+        third_thing: int
+
+        class Config:
+            fields = {'third_thing': 'Banana'}
+
+    v = ModelTwo(**{'oneThing': 123, 'anotherThing': '321', 'Banana': 1})
+    assert v == {'one_thing': 123, 'another_thing': 321, 'third_thing': 1}
+
+
+def test_pop_by_field_name():
+    class Model(BaseModel):
+        last_updated_by: Optional[str] = None
+
+        class Config:
+            extra = Extra.forbid
+            allow_population_by_field_name = True
+            fields = {'last_updated_by': 'lastUpdatedBy'}
+
+    assert Model(lastUpdatedBy='foo').dict() == {'last_updated_by': 'foo'}
+    assert Model(last_updated_by='foo').dict() == {'last_updated_by': 'foo'}
+    with pytest.raises(ValidationError) as exc_info:
+        Model(lastUpdatedBy='foo', last_updated_by='bar')
+    assert exc_info.value.errors() == [
+        {'loc': ('last_updated_by',), 'msg': 'extra fields not permitted', 'type': 'value_error.extra'}
+    ]
+
+
+def test_population_by_alias():
+    with pytest.warns(DeprecationWarning, match='"allow_population_by_alias" is deprecated and replaced by'):
+
+        class Model(BaseModel):
+            a: str
+
+            class Config:
+                allow_population_by_alias = True
+                fields = {'a': {'alias': '_a'}}
+
+    assert Model.__config__.allow_population_by_field_name is True
+    assert Model(a='different').a == 'different'
+    assert Model(a='different').dict() == {'a': 'different'}
+    assert Model(a='different').dict(by_alias=True) == {'_a': 'different'}
+
+
+def test_alias_child_precedence():
+    class Parent(BaseModel):
+        x: int
+
+        class Config:
+            fields = {'x': 'x1'}
+
+    class Child(Parent):
+        y: int
+
+        class Config:
+            fields = {'y': 'y2', 'x': 'x2'}
+
+    assert Child.__fields__['y'].alias == 'y2'
+    assert Child.__fields__['x'].alias == 'x2'
+
+
+def test_alias_generator_parent():
+    class Parent(BaseModel):
+        x: int
+
+        class Config:
+            allow_population_by_field_name = True
+
+            @classmethod
+            def alias_generator(cls, f_name):
+                return f_name + '1'
+
+    class Child(Parent):
+        y: int
+
+        class Config:
+            @classmethod
+            def alias_generator(cls, f_name):
+                return f_name + '2'
+
+    assert Child.__fields__['y'].alias == 'y2'
+    assert Child.__fields__['x'].alias == 'x2'
+
+
+def test_alias_generator_on_parent():
+    class Parent(BaseModel):
+        x: bool = Field(..., alias='a_b_c')
+        y: str
+
+        class Config:
+            @staticmethod
+            def alias_generator(x):
+                return x.upper()
+
+    class Child(Parent):
+        y: str
+        z: str
+
+    assert Parent.__fields__['x'].alias == 'a_b_c'
+    assert Parent.__fields__['y'].alias == 'Y'
+    assert Child.__fields__['x'].alias == 'a_b_c'
+    assert Child.__fields__['y'].alias == 'Y'
+    assert Child.__fields__['z'].alias == 'Z'
+
+
+def test_alias_generator_on_child():
+    class Parent(BaseModel):
+        x: bool = Field(..., alias='abc')
+        y: str
+
+    class Child(Parent):
+        y: str
+        z: str
+
+        class Config:
+            @staticmethod
+            def alias_generator(x):
+                return x.upper()
+
+    assert [f.alias for f in Parent.__fields__.values()] == ['abc', 'y']
+    assert [f.alias for f in Child.__fields__.values()] == ['abc', 'Y', 'Z']
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index 3f84cdfcde4..25a07f9f9fb 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -1,4 +1,3 @@
-import re
 import sys
 from decimal import Decimal
 from enum import Enum
@@ -7,7 +6,6 @@
 import pytest
 
 from pydantic import (
-    BaseConfig,
     BaseModel,
     BaseSettings,
     Extra,
@@ -328,49 +326,6 @@ class Model(BaseModel):
     assert Model(v={2: [1, 2, 3]}).dict() == {'v': {2: [1, 2, 3]}}
 
 
-def test_infer_alias():
-    class Model(BaseModel):
-        a = 'foobar'
-
-        class Config:
-            fields = {'a': '_a'}
-
-    assert Model(_a='different').a == 'different'
-    assert repr(Model.__fields__['a']) == (
-        "ModelField(name='a', type=str, required=False, default='foobar', alias='_a')"
-    )
-
-
-def test_alias_error():
-    class Model(BaseModel):
-        a = 123
-
-        class Config:
-            fields = {'a': '_a'}
-
-    assert Model(_a='123').a == 123
-
-    with pytest.raises(ValidationError) as exc_info:
-        Model(_a='foo')
-    assert exc_info.value.errors() == [
-        {'loc': ('_a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
-    ]
-
-
-def test_annotation_config():
-    class Model(BaseModel):
-        b: float
-        a: int = 10
-        _c: str
-
-        class Config:
-            fields = {'b': 'foobar'}
-
-    assert list(Model.__fields__.keys()) == ['b', 'a']
-    assert [f.alias for f in Model.__fields__.values()] == ['foobar', 'a']
-    assert Model(foobar='123').b == 123.0
-
-
 def test_success_values_include():
     class Model(BaseModel):
         a: int = 1
@@ -721,47 +676,6 @@ class Config:
     ]
 
 
-def test_alias_camel_case():
-    class Model(BaseModel):
-        one_thing: int
-        another_thing: int
-
-        class Config(BaseConfig):
-            @classmethod
-            def get_field_info(cls, name):
-                field_config = super().get_field_info(name) or {}
-                if 'alias' not in field_config:
-                    field_config['alias'] = re.sub(r'(?:^|_)([a-z])', lambda m: m.group(1).upper(), name)
-                return field_config
-
-    v = Model(**{'OneThing': 123, 'AnotherThing': '321'})
-    assert v.one_thing == 123
-    assert v.another_thing == 321
-    assert v == {'one_thing': 123, 'another_thing': 321}
-
-
-def test_get_field_info_inherit():
-    class ModelOne(BaseModel):
-        class Config(BaseConfig):
-            @classmethod
-            def get_field_info(cls, name):
-                field_config = super().get_field_info(name) or {}
-                if 'alias' not in field_config:
-                    field_config['alias'] = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
-                return field_config
-
-    class ModelTwo(ModelOne):
-        one_thing: int
-        another_thing: int
-        third_thing: int
-
-        class Config:
-            fields = {'third_thing': 'Banana'}
-
-    v = ModelTwo(**{'oneThing': 123, 'anotherThing': '321', 'Banana': 1})
-    assert v == {'one_thing': 123, 'another_thing': 321, 'third_thing': 1}
-
-
 def test_return_errors_ok():
     class Model(BaseModel):
         foo: int
@@ -846,24 +760,6 @@ class Model(BaseModel):
     assert Model(a=None).a is None
 
 
-def test_pop_by_alias():
-    class Model(BaseModel):
-        last_updated_by: Optional[str] = None
-
-        class Config:
-            extra = Extra.forbid
-            allow_population_by_field_name = True
-            fields = {'last_updated_by': 'lastUpdatedBy'}
-
-    assert Model(lastUpdatedBy='foo').dict() == {'last_updated_by': 'foo'}
-    assert Model(last_updated_by='foo').dict() == {'last_updated_by': 'foo'}
-    with pytest.raises(ValidationError) as exc_info:
-        Model(lastUpdatedBy='foo', last_updated_by='bar')
-    assert exc_info.value.errors() == [
-        {'loc': ('last_updated_by',), 'msg': 'extra fields not permitted', 'type': 'value_error.extra'}
-    ]
-
-
 def test_validate_all():
     class Model(BaseModel):
         a: int
@@ -1118,22 +1014,6 @@ class Model(BaseModel):
             foo: int = Schema(4)
 
 
-def test_population_by_alias():
-    with pytest.warns(DeprecationWarning, match='"allow_population_by_alias" is deprecated and replaced by'):
-
-        class Model(BaseModel):
-            a: str
-
-            class Config:
-                allow_population_by_alias = True
-                fields = {'a': {'alias': '_a'}}
-
-    assert Model.__config__.allow_population_by_field_name is True
-    assert Model(a='different').a == 'different'
-    assert Model(a='different').dict() == {'a': 'different'}
-    assert Model(a='different').dict(by_alias=True) == {'_a': 'different'}
-
-
 def test_fields_deprecated():
     class Model(BaseModel):
         v: str = 'x'
@@ -1145,46 +1025,6 @@ class Model(BaseModel):
     assert Model.__fields__.keys() == {'v'}
 
 
-def test_alias_child_precedence():
-    class Parent(BaseModel):
-        x: int
-
-        class Config:
-            fields = {'x': 'x1'}
-
-    class Child(Parent):
-        y: int
-
-        class Config:
-            fields = {'y': 'y2', 'x': 'x2'}
-
-    assert Child.__fields__['y'].alias == 'y2'
-    assert Child.__fields__['x'].alias == 'x2'
-
-
-def test_alias_generator_parent():
-    class Parent(BaseModel):
-        x: int
-
-        class Config:
-            allow_population_by_field_name = True
-
-            @classmethod
-            def alias_generator(cls, f_name):
-                return f_name + '1'
-
-    class Child(Parent):
-        y: int
-
-        class Config:
-            @classmethod
-            def alias_generator(cls, f_name):
-                return f_name + '2'
-
-    assert Child.__fields__['y'].alias == 'y2'
-    assert Child.__fields__['x'].alias == 'x2'
-
-
 def test_optional_field_constraints():
     class MyModel(BaseModel):
         my_int: Optional[int] = Field(..., ge=3)
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 3d5ee2cc3b4..bec0fa963e2 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -100,7 +100,7 @@ class Config:
             title = 'Apple Pie'
             fields = {'a': 'Snap', 'b': 'Crackle'}
 
-    s = {
+    assert ApplePie.schema() == {
         'type': 'object',
         'title': 'Apple Pie',
         'properties': {
@@ -109,11 +109,29 @@ class Config:
         },
         'required': ['Snap'],
     }
-    assert ApplePie.schema() == s
     assert list(ApplePie.schema(by_alias=True)['properties'].keys()) == ['Snap', 'Crackle']
     assert list(ApplePie.schema(by_alias=False)['properties'].keys()) == ['a', 'b']
 
 
+def test_by_alias_generator():
+    class ApplePie(BaseModel):
+        a: float
+        b: int = 10
+
+        class Config:
+            @staticmethod
+            def alias_generator(x):
+                return x.upper()
+
+    assert ApplePie.schema() == {
+        'title': 'ApplePie',
+        'type': 'object',
+        'properties': {'A': {'title': 'A', 'type': 'number'}, 'B': {'title': 'B', 'default': 10, 'type': 'integer'}},
+        'required': ['A'],
+    }
+    assert ApplePie.schema(by_alias=False)['properties'].keys() == {'a', 'b'}
+
+
 def test_sub_model():
     class Foo(BaseModel):
         """hello"""


Code review comments:
diff_hunk: @@ -73,14 +73,22 @@ class BaseConfig:
 
     @classmethod
     def get_field_info(cls, name: str) -> Dict[str, Any]:
-        field_info = cls.fields.get(name) or {}
-        if isinstance(field_info, str):
-            field_info = {'alias': field_info}
-        elif cls.alias_generator and 'alias' not in field_info:
+        fields_value = cls.fields.get(name)
+
+        if isinstance(fields_value, str):
+            field_info: Dict[str, Any] = {'alias': fields_value, 'alias_priority': 0}
path: pydantic/main.py
text: @author:
```suggestion
            field_info: Dict[str, Any] = {'alias': fields_value}
```

diff_hunk: @@ -73,14 +73,22 @@ class BaseConfig:
 
     @classmethod
     def get_field_info(cls, name: str) -> Dict[str, Any]:
-        field_info = cls.fields.get(name) or {}
-        if isinstance(field_info, str):
-            field_info = {'alias': field_info}
-        elif cls.alias_generator and 'alias' not in field_info:
+        fields_value = cls.fields.get(name)
+
+        if isinstance(fields_value, str):
+            field_info: Dict[str, Any] = {'alias': fields_value, 'alias_priority': 0}
+        elif isinstance(fields_value, dict):
+            field_info = fields_value
+        else:
+            field_info = {}
+
+        if 'alias' in field_info:
+            field_info['alias_priority'] = 2
path: pydantic/main.py
text: @author:
```suggestion
            field_info.setdefault('alias_priority', 2)
```