Search code examples
pythonpydantic

Pydantic throwing extra attribute error when an alias is used


I have a small python example using pydantic that shows my issue:

from typing import Literal
from pydantic import BaseModel, Field, ConfigDict


class Base(BaseModel):
    # This method allows inherited classes to be subscriptable like a dictionary i.e value = class['key']
    def __getitem__(self, item):
        return getattr(self, item)

    model_config = ConfigDict(extra='forbid')

class Foo(Base):
    bar: Literal['bar1', 'bar2'] = Field(default=None, frozen=True, alias="bar_path")

a = {'bar': 'bar1'}
b = Foo(**a)

This causes the following exception from pydantic:

Traceback (most recent call last):
  File "/home/hawk/applications/pydantic_example.py", line 16, in <module>
    b = Foo(**a)
        ^^^^^^^^
  File "/home/hawk/miniconda3/envs/bird/lib/python3.11/site-packages/pydantic/main.py", line 164, in __init__
    __pydantic_self__.__pydantic_validator__.validate_python(data, self_instance=__pydantic_self__)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Foo
bar
  Extra inputs are not permitted [type=extra_forbidden, input_value='bar1', input_type=str]
    For further information visit https://errors.pydantic.dev/2.5/v/extra_forbidden

If i remove the alias then it works fine. If i have the alias but change it to

a = {'bar_path': 'bar1'}

then it works. Any idea why the alias is being treated as an extra parameter?


Solution

  • I think Pydantic v2 requires explicitly defining the choice of aliases, using the AliasChoice class. See the following example:

    from typing import Literal
    from pydantic import BaseModel, Field, ConfigDict, AliasChoices
    
    
    class Base(BaseModel):
        def __getitem__(self, item):
            return getattr(self, item)
    
        model_config = ConfigDict(extra='forbid')
    
    class Foo(Base):
        bar: Literal['bar1', 'bar2'] = Field(default=None, frozen=True, alias=AliasChoices('bar', 'bar_path'))
    
    a = {'bar': 'bar1'}
    b = Foo(**a)
    print(b)
    
    c = Foo(bar_path='bar2')
    print(c)
    

    Which outputs:

    bar='bar1'
    bar='bar2'
    

    This makes both names work. My initial expectation was the field name would always work as well, but it seems that is not the case.

    I hope this helps!