Skip to content

Inspect type aliases to determine if an annotation is complex #644

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

Merged
merged 4 commits into from
Jul 6, 2025

Conversation

tselepakis
Copy link
Contributor

@tselepakis tselepakis commented Jun 26, 2025

This PR ensures that type aliases declared using type (PEP 695) are correctly resolved and handled during environment value parsing and model field introspection.

I aimed to keep the changes minimal, but addressing compatibility issues required introducing a couple of new patterns, specifically to work around syntax errors triggered by pytest when using Python versions <3.12, as well as ruff linter errors. If there are better or more idiomatic approaches, I'm happy to discuss them. I'm not certain the current solutions are the best long-term fit for the project.

Fixes #536

@tselepakis tselepakis force-pushed the issue-536 branch 2 times, most recently from 6a5e7ad to 5f9a0bf Compare June 26, 2025 21:04
@hramezani hramezani requested a review from Viicos June 26, 2025 21:28
@Viicos
Copy link
Member

Viicos commented Jun 27, 2025

Thanks @tselepakis, I would suggest using the following patch instead:

diff --git a/pydantic_settings/sources/base.py b/pydantic_settings/sources/base.py
index b2c4d16..881546b 100644
--- a/pydantic_settings/sources/base.py
+++ b/pydantic_settings/sources/base.py
@@ -16,6 +16,7 @@ from pydantic._internal._typing_extra import (  # type: ignore[attr-defined]
 from pydantic._internal._utils import is_model_class
 from pydantic.fields import FieldInfo
 from typing_extensions import get_args
+from typing_inspection import typing_objects
 from typing_inspection.introspection import is_union_origin
 
 from ..exceptions import SettingsError
@@ -353,6 +354,12 @@ class PydanticBaseEnvSettingsSource(PydanticBaseSettingsSource):
                 field_info.append((v_alias, self._apply_case_sensitive(v_alias), False))
 
         if not v_alias or self.config.get('populate_by_name', False):
+            if typing_objects.is_typealiastype(field.annotation) or typing_objects.is_typealiastype(get_origin(field.annotation)):
+                annotation = field.annotation.__value__
+            else:
+                annotation = field.annotation
+            if typing_objects.is_annotated(get_origin(annotation)):
+                annotation = annotation.__origin__
             if is_union_origin(get_origin(field.annotation)) and _union_is_complex(field.annotation, field.metadata):
                 field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), True))
             else:
diff --git a/pydantic_settings/sources/utils.py b/pydantic_settings/sources/utils.py
index 270a8c1..498a8dc 100644
--- a/pydantic_settings/sources/utils.py
+++ b/pydantic_settings/sources/utils.py
@@ -43,6 +43,9 @@ def parse_env_vars(
 def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool:
     # If the model is a root model, the root annotation should be used to
     # evaluate the complexity.
+    if typing_objects.is_typealiastype(annotation) or typing_objects.is_typealiastype(get_origin(annotation)):
+        annotation = annotation.__value__
+
     if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel:
         annotation = cast('type[RootModel[Any]]', annotation)
         root_annotation = annotation.model_fields['root'].annotation

That's really hacky and looking at the code related to this I'm pretty sure it contains many other bugs, but we can't do better considering the current state of pydantic-settings.

@tselepakis
Copy link
Contributor Author

Thanks for the review and the detailed patch, @Viicos. I appreciate you taking the time to provide a solution.

I have a question regarding the first change in base.py. You've introduced a new annotation variable that strips away TypeAlias and Annotated wrappers.

However, the is_union_origin check immediately after that, still uses get_origin(field.annotation) instead of get_origin(annotation). Was this intentional, or should the is_union_origin check also use the unwrapped annotation variable?

Additionally, could you please explain the specific bug or use case that this logic is intended to cover (if I remove this part my test still passes, perhaps we need another test to cover this case)? I'm trying to understand which scenario requires us to check for is_typealiastype and is_annotated here. As you mentioned that the solution is "hacky", understanding the exact case would be very helpful for me.

@Viicos
Copy link
Member

Viicos commented Jun 30, 2025

However, the is_union_origin check immediately after that, still uses get_origin(field.annotation) instead of get_origin(annotation). Was this intentional, or should the is_union_origin check also use the unwrapped annotation variable?

That's an oversight sorry, it should check annotation, not field.annotation.

Additionally, could you please explain the specific bug or use case that this logic is intended to cover (if I remove this part my test still passes, perhaps we need another test to cover this case)? I'm trying to understand which scenario requires us to check for is_typealiastype and is_annotated here. As you mentioned that the solution is "hacky", understanding the exact case would be very helpful for me.

I believe both options would have worked but yours was more likely to cause unexpected issues as you implemented the type alias "unpacking" in get_origin()/get_args(), which in theory can be used anywhere in the code base. My proposed change is only scoped to the places where it is necessary to unpack them.

@Viicos Viicos changed the title Fix: Handle type alias declared with type statement Inspect type aliases to determine if an annotation is complex Jun 30, 2025
Copy link
Member

@Viicos Viicos left a comment

Choose a reason for hiding this comment

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

Thanks @tselepakis!

@Viicos Viicos merged commit 50dedf7 into pydantic:main Jul 6, 2025
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pydantic Settings cannot deserialize JSON value of type declared with PEP-695 style type alias
2 participants