Skip to content

NoDecode not working when the field's annotation is defined by using the type statement instead of through simple assignment #715

@Liang0info

Description

@Liang0info

According to Python's Document Type aliases, type aliases can be created through simple assignment or using the type statement. The two ways should be equivalent. However, as the example code below, If a field's annotation is a type aliases defined by using the type statement, the NoDecode won't working.

Example Code

from typing import Any, Annotated
from pydantic import BeforeValidator
from pydantic_settings import NoDecode, BaseSettings


def _parse_comma_sep_str_list(value: Any) -> list[str]:
    if not value:
        return []
    if isinstance(value, str):
        return value.split(',')
    return value


CommaSepStrListA = Annotated[list[str], NoDecode, BeforeValidator(_parse_comma_sep_str_list)]
type CommaSepStrListB = Annotated[list[str], NoDecode, BeforeValidator(_parse_comma_sep_str_list)]


class MySettingsA(BaseSettings):
    enabled_options: CommaSepStrListA = ["A", "B", "C", "D"]


class MySettingsB(BaseSettings):
    enabled_options: CommaSepStrListB = ["A", "B", "C", "D"]


if __name__ == "__main__":
    import os
    os.environ["ENABLED_OPTIONS"] = "A,B,C,D"
    my_settings_a = MySettingsA()
    print(my_settings_a)
    my_settings_b = MySettingsB()
    print(my_settings_b)

The result

MySettingsA works well, and MySettingsB will try to parse the enabled options as JSON.

enabled_options=['A', 'B', 'C', 'D']
Traceback (most recent call last):
  File "E:\pythonProject\.venv\Lib\site-packages\pydantic_settings\sources\base.py", line 510, in __call__
    field_value = self.prepare_field_value(field_name, field, field_value, value_is_complex)
  File "E:\pythonProject\.venv\Lib\site-packages\pydantic_settings\sources\providers\env.py", line 123, in prepare_field_value
    raise e
  File "E:\pythonProject\.venv\Lib\site-packages\pydantic_settings\sources\providers\env.py", line 120, in prepare_field_value
    value = self.decode_complex_value(field_name, field, value)
  File "E:\pythonProject\.venv\Lib\site-packages\pydantic_settings\sources\base.py", line 187, in decode_complex_value
    return json.loads(value)
           ~~~~~~~~~~^^^^^^^
  File "C:\Program Files\Python313\Lib\json\__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "C:\Program Files\Python313\Lib\json\decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python313\Lib\json\decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "E:\pythonProject\my_settings.py", line 31, in <module>
    my_settings_b = MySettingsB()
  File "E:\pythonProject\.venv\Lib\site-packages\pydantic_settings\main.py", line 195, in __init__
    **__pydantic_self__._settings_build_values(
      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        values,
        ^^^^^^^
    ...<25 lines>...
        _secrets_dir=_secrets_dir,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "E:\pythonProject\.venv\Lib\site-packages\pydantic_settings\main.py", line 438, in _settings_build_values
    source_state = source()
  File "E:\pythonProject\.venv\Lib\site-packages\pydantic_settings\sources\base.py", line 512, in __call__
    raise SettingsError(
        f'error parsing value for field "{field_name}" from source "{self.__class__.__name__}"'
    ) from e
pydantic_settings.exceptions.SettingsError: error parsing value for field "enabled_options" from source "EnvSettingsSource"

Expected Behavior

enabled_options=['A', 'B', 'C', 'D']
enabled_options=['A', 'B', 'C', 'D']

Additional Info

However, if I instantiate the BaseSettings, like instantiate the BaseModel in Pydantic, instead of setting the env, it works as expected.

if __name__ == "__main__":
    my_settings_a = MySettingsA(enabled_options="A,B,C,D")
    print(my_settings_a)
    my_settings_b = MySettingsB(enabled_options="A,B,C,D")
    print(my_settings_b)

The result:

enabled_options=['A', 'B', 'C', 'D']
enabled_options=['A', 'B', 'C', 'D']

When I print the FieldInfo of MySettingsA and MySettingsB, I found they have different FieldInfo, it seems metadata was lost in MySettingsB. However, BeforeValidator works well in MySettingsB.

if __name__ == "__main__":
    print(MySettingsA.model_fields.items())
    print(MySettingsB.model_fields.items())
dict_items([('enabled_options', FieldInfo(annotation=list[str], required=False, default=['A', 'B', 'C', 'D'], metadata=[<class 'pydantic_settings.sources.types.NoDecode'>, BeforeValidator(func=<function _parse_comma_sep_str_list at 0x0000024FA30B3B00>, json_schema_input_type=PydanticUndefined)]))])
dict_items([('enabled_options', FieldInfo(annotation=CommaSepStrListB, required=False, default=['A', 'B', 'C', 'D']))])

Python, Pydantic & OS Version

python -c "import pydantic.version; print(pydantic.version.version_info())"
             pydantic version: 2.12.4
        pydantic-core version: 2.41.5
          pydantic-core build: profile=release pgo=false
               python version: 3.13.9 (tags/v3.13.9:8183fa5, Oct 14 2025, 14:09:13) [MSC v.1944 64 bit (AMD64)]
                     platform: Windows-10-10.0.19045-SP0
             related packages: pydantic-settings-2.12.0 typing_extensions-4.15.0
                       commit: unknown
``

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions