Repository: pydantic/pydantic
Pull request: Make generic TypeAliasType specifications produce different schema definitions
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->

## Change Summary

This PR fixes #7840: now different specifications of a generic TypeAliasType produce different definitions in pydantic schema which prevents "overwrites" of other specifications with the same origin.

## Related issue number

<!-- WARNING: please use "fix #123" style references so the issue is closed when this PR is merged. -->
* Fix #7840 

## Checklist

* [x] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [x] Documentation reflects the changes where applicable
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Selected Reviewer: @sydney-runkle

Problem statement:
PEP 695 generic type alias does not work
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

PEP 695 generic type alias does not seem to work. JSON schema also does not look right. Looks like the first type definition is somehow cached and re-used incorrectly. See MRE, python 3.12+. I also tried v2.4.2, but the output is the same

### Example Code

```Python
from collections.abc import Sequence
from typing import Annotated

from pydantic import BaseModel, Field, ValidationError

type NonEmptySequence[T] = Annotated[
    Sequence[T],
    Field(min_length=1, max_length=1024)
]


class ModelA(BaseModel):
    a: int


class ModelB(BaseModel):
    b: str


class Model(BaseModel):
    a_s: NonEmptySequence[ModelA]
    b_s: NonEmptySequence[ModelB]


print(Model.model_json_schema())

try:
    model = Model(
        a_s=[ModelA(a=1)],
        b_s=[ModelB(b='test')]
    )
except ValidationError as error:
    print(error)
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=false
                 install path: /.venv/lib/python3.12/site-packages/pydantic
               python version: 3.12.0 (v3.12.0:0fb18b02c8, Oct  2 2023, 09:45:56) [Clang 13.0.0 (clang-1300.0.29.30)]
                     platform: macOS-13.0.1-arm64-arm-64bit
     optional deps. installed: ['email-validator', 'typing-extensions']
```

Hints from issue discussion:
HI @AlexanderPodorov,

Thanks for reporting this. As the docs state [here](https://docs.pydantic.dev/latest/concepts/types/#named-type-aliases), we do support generic type aliases. Indeed looks like a bug 🐛.
Thank you @sydney-runkle !
The docs are using `TypeAliasType` backport, not sure is there any difference to the new `type` statement introduced in PEP 695.
Also I wanted to note that I did not test it using `TypeAliasType`.
> Also I wanted to note that I did not test it using TypeAliasType.

Hi, I have also faced this bug and can confirm that the backport of `TypeAliasType` on Python 3.9 has the exactly same problem:

```python
from collections.abc import Sequence
from typing import Annotated, TypeVar

from pydantic import BaseModel, Field, ValidationError
from typing_extensions import TypeAliasType

T = TypeVar('T')
NonEmptySequence = TypeAliasType(
    'NonEmptySequence',
    Annotated[
        Sequence[T],
        Field(min_length=1, max_length=1024)
    ],
    type_params=(T,)
)


class ModelA(BaseModel):
    a: int


class ModelB(BaseModel):
    b: str


class Model(BaseModel):
    a_s: NonEmptySequence[ModelA]
    b_s: NonEmptySequence[ModelB]


print(Model.model_json_schema())

try:
    model = Model(
        a_s=[ModelA(a=1)],
        b_s=[ModelB(b='test')]
    )
except ValidationError as error:
    print(error)
```
output:
```bash
{'$defs': {'ModelA': {'properties': {'a': {'title': 'A', 'type': 'integer'}}, 'required': ['a'], 'title': 'ModelA', 'type': 'object'}, 'NonEmptySequence': {'items': {'$ref': '#/$defs/ModelA'}, 'minLength': 1, 'type': 'array'}}, 'properties': {'a_s': {'$ref': '#/$defs/NonEmptySequence'}, 'b_s': {'$ref': '#/$defs/NonEmptySequence'}}, 'required': ['a_s', 'b_s'], 'title': 'Model', 'type': 'object'}
1 validation error for Model
b_s.0
  Input should be a valid dictionary or instance of ModelA [type=model_type, input_value=ModelB(b='test'), input_type=ModelB]
    For further information visit https://errors.pydantic.dev/2.4/v/model_type
```

Commit message: Fix typecheck

Patch to review:
diff --git a/pydantic/_internal/_core_utils.py b/pydantic/_internal/_core_utils.py
index ebf12ec0fa1..46a27062eef 100644
--- a/pydantic/_internal/_core_utils.py
+++ b/pydantic/_internal/_core_utils.py
@@ -8,13 +8,14 @@
     Hashable,
     TypeVar,
     Union,
-    _GenericAlias,  # type: ignore
     cast,
 )
 
 from pydantic_core import CoreSchema, core_schema
 from pydantic_core import validate_core_schema as _validate_core_schema
-from typing_extensions import TypeAliasType, TypeGuard, get_args
+from typing_extensions import TypeAliasType, TypeGuard, get_args, get_origin
+
+from pydantic._internal._typing_extra import is_generic_alias
 
 from . import _repr
 
@@ -86,8 +87,10 @@ def get_type_ref(type_: type[Any], args_override: tuple[type[Any], ...] | None =
     This `args_override` argument was added for the purpose of creating valid recursive references
     when creating generic models without needing to create a concrete class.
     """
-    origin = type_
-    args = get_args(type_) if isinstance(type_, _GenericAlias) else (args_override or ())
+    origin = get_origin(type_)
+    origin = origin or type_
+
+    args = get_args(type_) if is_generic_alias(type_) else (args_override or ())
     generic_metadata = getattr(type_, '__pydantic_generic_metadata__', None)
     if generic_metadata:
         origin = generic_metadata['origin'] or origin
diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py
index ef929aba95d..4ed59606b4a 100644
--- a/pydantic/_internal/_generate_schema.py
+++ b/pydantic/_internal/_generate_schema.py
@@ -1052,18 +1052,21 @@ def _type_alias_type_schema(
         self,
         obj: Any,  # TypeAliasType
     ) -> CoreSchema:
-        origin = get_origin(obj)
-        origin = origin or obj
-        with self.defs.get_schema_or_ref(origin) as (ref, maybe_schema):
+        with self.defs.get_schema_or_ref(obj) as (ref, maybe_schema):
             if maybe_schema is not None:
                 return maybe_schema
 
+            origin = get_origin(obj)
+            origin = origin or obj
+
             namespace = (self._types_namespace or {}).copy()
             new_namespace = {**_typing_extra.get_cls_types_namespace(origin), **namespace}
             annotation = origin.__value__
 
             self._types_namespace = new_namespace
             typevars_map = get_standard_typevars_map(obj)
+
+            annotation = _typing_extra.eval_type_lenient(annotation, self._types_namespace, None)
             annotation = replace_types(annotation, typevars_map)
             schema = self.generate_schema(annotation)
             assert schema['type'] != 'definitions'
diff --git a/pydantic/_internal/_typing_extra.py b/pydantic/_internal/_typing_extra.py
index e83e03d2b8e..7556b6ff463 100644
--- a/pydantic/_internal/_typing_extra.py
+++ b/pydantic/_internal/_typing_extra.py
@@ -433,3 +433,10 @@ def is_dataclass(_cls: type[Any]) -> TypeGuard[type[StandardDataclass]]:
 
 def origin_is_type_alias_type(origin: Any) -> TypeGuard[TypeAliasType]:
     return isinstance(origin, TypeAliasType)
+
+
+def is_generic_alias(type_: type[Any]) -> bool:
+    if sys.version_info >= (3, 10):
+        return isinstance(type_, (types.GenericAlias, typing._GenericAlias))  # type: ignore[attr-defined]
+    else:
+        return isinstance(type_, typing._GenericAlias)  # type: ignore
diff --git a/tests/test_type_alias_type.py b/tests/test_type_alias_type.py
index d024cf9da17..19f982f4ca6 100644
--- a/tests/test_type_alias_type.py
+++ b/tests/test_type_alias_type.py
@@ -6,7 +6,7 @@
 from annotated_types import MaxLen
 from typing_extensions import Annotated, TypeAliasType
 
-from pydantic import Field, ValidationError
+from pydantic import BaseModel, Field, ValidationError
 from pydantic.type_adapter import TypeAdapter
 
 T = TypeVar('T')
@@ -185,14 +185,15 @@ def test_type_alias_annotated_defs() -> None:
         'type': 'array',
         'minItems': 2,
         'prefixItems': [
-            {'$ref': '#/$defs/MyList_MaxLen_max_length_1_'},
-            {'$ref': '#/$defs/MyList_MaxLen_max_length_1_'},
+            {'$ref': '#/$defs/MyList_int__MaxLen_max_length_1_'},
+            {'$ref': '#/$defs/MyList_int__MaxLen_max_length_1_'},
         ],
         'maxItems': 2,
-        '$defs': {'MyList_MaxLen_max_length_1_': {'type': 'array', 'items': {'type': 'integer'}, 'maxItems': 1}},
+        '$defs': {'MyList_int__MaxLen_max_length_1_': {'type': 'array', 'items': {'type': 'integer'}, 'maxItems': 1}},
     }
 
 
+@pytest.mark.xfail(sys.version_info >= (3, 12), reason="TypeAliasType doesn't have __qualname__ yet")
 def test_recursive_generic_type_alias() -> None:
     t = TypeAdapter(RecursiveGenericAlias[int])
 
@@ -223,11 +224,11 @@ def test_recursive_generic_type_alias() -> None:
     ]
 
     assert t.json_schema() == {
-        'allOf': [{'$ref': '#/$defs/RecursiveGenericAlias'}],
+        'allOf': [{'$ref': '#/$defs/RecursiveGenericAlias_int_'}],
         '$defs': {
-            'RecursiveGenericAlias': {
+            'RecursiveGenericAlias_int_': {
                 'type': 'array',
-                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias'}, {'type': 'integer'}]},
+                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias_int_'}, {'type': 'integer'}]},
             }
         },
     }
@@ -254,12 +255,12 @@ def test_recursive_generic_type_alias_annotated() -> None:
     # insert_assert(t.json_schema())
     assert t.json_schema() == {
         'type': 'array',
-        'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias'}, {'type': 'integer'}]},
+        'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias_int_'}, {'type': 'integer'}]},
         'maxItems': 1,
         '$defs': {
-            'RecursiveGenericAlias': {
+            'RecursiveGenericAlias_int_': {
                 'type': 'array',
-                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias'}, {'type': 'integer'}]},
+                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias_int_'}, {'type': 'integer'}]},
             }
         },
     }
@@ -289,18 +290,18 @@ def test_recursive_generic_type_alias_annotated_defs() -> None:
         'type': 'array',
         'minItems': 2,
         'prefixItems': [
-            {'$ref': '#/$defs/RecursiveGenericAlias_MaxLen_max_length_1_'},
-            {'$ref': '#/$defs/RecursiveGenericAlias_MaxLen_max_length_1_'},
+            {'$ref': '#/$defs/RecursiveGenericAlias_int__MaxLen_max_length_1_'},
+            {'$ref': '#/$defs/RecursiveGenericAlias_int__MaxLen_max_length_1_'},
         ],
         'maxItems': 2,
         '$defs': {
-            'RecursiveGenericAlias': {
+            'RecursiveGenericAlias_int_': {
                 'type': 'array',
-                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias'}, {'type': 'integer'}]},
+                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias_int_'}, {'type': 'integer'}]},
             },
-            'RecursiveGenericAlias_MaxLen_max_length_1_': {
+            'RecursiveGenericAlias_int__MaxLen_max_length_1_': {
                 'type': 'array',
-                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias'}, {'type': 'integer'}]},
+                'items': {'anyOf': [{'$ref': '#/$defs/RecursiveGenericAlias_int_'}, {'type': 'integer'}]},
                 'maxItems': 1,
             },
         },
@@ -319,3 +320,26 @@ def test_field() -> None:
         'allOf': [{'$ref': '#/$defs/SomeAlias'}],
         'title': 'abc',
     }
+
+
+def test_nested_generic_type_alias_type() -> None:
+    class MyModel(BaseModel):
+        field_1: MyList[bool]
+        field_2: MyList[str]
+
+    model = MyModel(field_1=[True], field_2=['abc'])
+
+    assert model.model_json_schema() == {
+        '$defs': {
+            'MyList_bool_': {'items': {'type': 'boolean'}, 'type': 'array'},
+            'MyList_str_': {'items': {'type': 'string'}, 'type': 'array'},
+        },
+        'properties': {'field_1': {'$ref': '#/$defs/MyList_bool_'}, 'field_2': {'$ref': '#/$defs/MyList_str_'}},
+        'required': ['field_1', 'field_2'],
+        'title': 'MyModel',
+        'type': 'object',
+    }
+
+
+def test_non_specified_generic_type_alias_type() -> None:
+    assert TypeAdapter(MyList).json_schema() == {'items': {}, 'type': 'array'}


Code review comments:
diff_hunk: @@ -86,8 +87,10 @@ def get_type_ref(type_: type[Any], args_override: tuple[type[Any], ...] | None =
     This `args_override` argument was added for the purpose of creating valid recursive references
     when creating generic models without needing to create a concrete class.
     """
-    origin = type_
-    args = get_args(type_) if isinstance(type_, _GenericAlias) else (args_override or ())
+    origin = get_origin(type_)
+    origin = origin or type_
path: pydantic/_internal/_core_utils.py
text: @user1:
```suggestion
    origin = get_origin(type_) or type_
```

diff_hunk: @@ -185,14 +185,15 @@ def test_type_alias_annotated_defs() -> None:
         'type': 'array',
         'minItems': 2,
         'prefixItems': [
-            {'$ref': '#/$defs/MyList_MaxLen_max_length_1_'},
-            {'$ref': '#/$defs/MyList_MaxLen_max_length_1_'},
+            {'$ref': '#/$defs/MyList_int__MaxLen_max_length_1_'},
+            {'$ref': '#/$defs/MyList_int__MaxLen_max_length_1_'},
         ],
         'maxItems': 2,
-        '$defs': {'MyList_MaxLen_max_length_1_': {'type': 'array', 'items': {'type': 'integer'}, 'maxItems': 1}},
+        '$defs': {'MyList_int__MaxLen_max_length_1_': {'type': 'array', 'items': {'type': 'integer'}, 'maxItems': 1}},
     }
 
 
+@pytest.mark.xfail(sys.version_info >= (3, 12), reason="TypeAliasType doesn't have __qualname__ yet")
path: tests/test_type_alias_type.py
text: @user1:
Based on the changes in #7929, I think you can remove this xfail marker

diff_hunk: @@ -8,13 +8,14 @@
     Hashable,
     TypeVar,
     Union,
-    _GenericAlias,  # type: ignore
     cast,
 )
 
 from pydantic_core import CoreSchema, core_schema
 from pydantic_core import validate_core_schema as _validate_core_schema
-from typing_extensions import TypeAliasType, TypeGuard, get_args
+from typing_extensions import TypeAliasType, TypeGuard, get_args, get_origin
+
+from pydantic._internal._typing_extra import is_generic_alias
path: pydantic/_internal/_core_utils.py
text: @user1:
```suggestion
._typing_extra import is_generic_alias
```

diff_hunk: @@ -1052,18 +1052,21 @@ def _type_alias_type_schema(
         self,
         obj: Any,  # TypeAliasType
     ) -> CoreSchema:
-        origin = get_origin(obj)
-        origin = origin or obj
-        with self.defs.get_schema_or_ref(origin) as (ref, maybe_schema):
+        with self.defs.get_schema_or_ref(obj) as (ref, maybe_schema):
             if maybe_schema is not None:
                 return maybe_schema
 
+            origin = get_origin(obj)
+            origin = origin or obj
path: pydantic/_internal/_generate_schema.py
text: @user1:
```suggestion
            origin = get_origin(obj) or obj
```

diff_hunk: @@ -433,3 +433,10 @@ def is_dataclass(_cls: type[Any]) -> TypeGuard[type[StandardDataclass]]:
 
 def origin_is_type_alias_type(origin: Any) -> TypeGuard[TypeAliasType]:
     return isinstance(origin, TypeAliasType)
+
+
+def is_generic_alias(type_: type[Any]) -> bool:
+    if sys.version_info >= (3, 10):
+        return isinstance(type_, (types.GenericAlias, typing._GenericAlias))  # type: ignore[attr-defined]
+    else:
+        return isinstance(type_, typing._GenericAlias)  # type: ignore
path: pydantic/_internal/_typing_extra.py
text: @user1:
```suggestion
if sys.version_info >= (3, 10):
    def is_generic_alias(type_: type[Any]) -> bool:
        return isinstance(type_, (types.GenericAlias, typing._GenericAlias))  # type: ignore[attr-defined] 
else:
    def is_generic_alias(type_: type[Any]) -> bool:
        return isinstance(type_, typing._GenericAlias)  # type: ignore
```
Super minor, but eliminates a runtime branch 🤷‍♂️