Repository: litestar-org/litestar

Issue:
Bug: Openapi schema generation crashes for models with `Annotated` type attribute
### Summary

I have a pydantic model that uses a list of [discriminated unions](https://docs.pydantic.dev/usage/types/#nested-discriminated-unions). Because of the list I need to annotate the union with the discriminator. But then the schema generation throws an error that it can't map the type.

### Basic Example

```python
class Base(BaseModel):
    pass

class A(Base):
    type: Literal["a"] = "a"

class B(Base):
    type: Literal["b"] = "b"

MyUnion = Annotated[Union[A, B], Field(discriminator="type")]

class Test(BaseModel):
  entries: List[MyUnion] = []
```
If you now use ``Test`` as a parameter in a handler function and open for example the swagger documentation you get an error like

> 500: Parameter '' with type 'typing.Annotated[typing.Union[A, B], FieldInfo(default=PydanticUndefined, discriminator='type', extra={})]' could not be mapped to an Open API type. This can occur if a user-defined generic type is resolved as a parameter. If '' should not be documented as a parameter, annotate it using the `Dependency` function, e.g., `: ... = Dependency(...)`.

### Drawbacks and Impact

_No response_

### Unresolved questions

_No response_


Discussion:
Hey, I tried to reproduce, can you look at this and tell me what to change to get it to fail please?

```py
"""Minimal Starlite application."""
from __future__ import annotations

from typing import Annotated, Any, List, Literal, Union

from pydantic import BaseModel, Field
from starlite import Starlite, get


class Base(BaseModel):
    """A base model."""


class Foo(Base):
    """A Foo model."""

    type: Literal["foo"] = "foo"


class Bar(Base):
    """A Bar model."""

    type: Literal["bar"] = "bar"


AnnotatedUnion = Annotated[Union[Foo, Bar], Field(discriminator="type")]


class Test(BaseModel):
    """A Test model."""

    entries: List[AnnotatedUnion] = []


@get("/")
def hello_world(test: Test) -> dict[str, Any]:
    """Route Handler that outputs hello world."""
    return {"hello": "world"}


app = Starlite(route_handlers=[hello_world])
```

On starlite 1.51, python 3.11, I get:

![image](https://user-images.githubusercontent.com/20659309/228086908-2c5186af-387d-4191-9aa5-39adc1d98550.png)

from `/schema/swagger`
When I comment out ``from __future__ import annotations``, I get the error.

I'm running ``Python 3.10.9`` on top of the ``main`` (commit 23b84b67dd20d2836caf8e3b54797b2d15f42e7e) branch.

I seems that import is still needed in 3.10. Is starlite intentionally using the new behaviour? 
Grepping through the code it is used alot, but I can't find any mention of it in the documentation.

The future import is very easy to miss. Is it possible to fix this when the user code does not do the import?
Oh - you're on main, OK that changes things. There were some pretty big changes to openapi internals for v2 which are in main and not in 1.51.

I have reproduced, thanks for the extra info.

> The future import is very easy to miss. Is it possible to fix this when the user code does not do the import?

It should work either way - I just use it without thinking.
I got the same problem, which is weird because I'm 95% sure that this use to work with my project (and I _hate_ setting hard constraints on my dependency versions, so I probably updated starlite to a newer version without noticing).

Note: i'm on startlite 1.51.7

I tested @peterschutt 's example, and it *does* work when you're using the `Test` class as an input time. However, if I happen to use the `AnnotatedUnion` as an input type (which i'm doing), I get the same error:

```python
"""Minimal Starlite application."""
from __future__ import annotations

from typing import Annotated, Any, Literal

import uvicorn
from pydantic import BaseModel, Field
from starlite import Starlite, get


class Base(BaseModel):
    """A base model."""


class Foo(Base):
    """A Foo model."""

    type: Literal["foo"] = "foo"


class Bar(Base):
    """A Bar model."""

    type: Literal["bar"] = "bar"


AnnotatedUnion = Annotated[Foo | Bar, Field(discriminator="type")]


@get("/")
def hello_world(test: AnnotatedUnion) -> dict[str, Any]:
    """Route Handler that outputs hello world."""
    return {"hello": "world"}


app = Starlite(route_handlers=[hello_world])

if __name__ == '__main__':
    uvicorn.run(app)
```

```
starlite.exceptions.http_exceptions.ImproperlyConfiguredException: 500: Parameter 'test' with type 'typing.Annotated[__main__.Foo | __main__.Bar, FieldInfo(default=PydanticUndefined, discriminator='type', extra={})]' could not be mapped to an Open API type. This can occur if a user-defined generic type is resolved as a parameter. If 'test' should not be documented as a parameter, annotate it using the `Dependency` function, e.g., `test: ... = Dependency(...)`.
```

Any idea on how to fix this as well (until some update comes along)?

EDIT: ok, I tested it on v2.0.0alpha2, and using the `AnnotatedUnion` as a type annot for `test` does work on this version.

Resolving patch:
diff --git a/starlite/_openapi/schema_generation/schema.py b/starlite/_openapi/schema_generation/schema.py
--- a/starlite/_openapi/schema_generation/schema.py
+++ b/starlite/_openapi/schema_generation/schema.py
@@ -27,11 +27,11 @@
     Set,
     Tuple,
     cast,
-    get_args,
 )
 from uuid import UUID
 
 from _decimal import Decimal
+from typing_extensions import get_args, get_type_hints
 
 from starlite._openapi.schema_generation.constrained_fields import (
     create_constrained_field_schema,
@@ -530,16 +530,17 @@ def create_schema_for_pydantic_model(
     Returns:
         A schema instance.
     """
+    field_type_hints = get_type_hints(field_type, include_extras=False)
     return Schema(
         required=[field.alias or field.name for field in field_type.__fields__.values() if field.required],
         properties={
             (f.alias or f.name): create_schema(
-                field=SignatureField.create(field_type=v, name=f.alias or f.name),
+                field=SignatureField.create(field_type=field_type_hints[f.name], name=f.alias or f.name),
                 generate_examples=generate_examples,
                 plugins=plugins,
                 schemas=schemas,
             )
-            for f, v in zip(field_type.__fields__.values(), field_type.__annotations__.values())
+            for f in field_type.__fields__.values()
         },
         type=OpenAPIType.OBJECT,
         title=_get_type_schema_name(field_type),
@@ -575,7 +576,7 @@ def create_schema_for_dataclass(
                 plugins=plugins,
                 schemas=schemas,
             )
-            for k, v in field_type.__annotations__.items()
+            for k, v in get_type_hints(field_type, include_extras=False).items()
         },
         type=OpenAPIType.OBJECT,
         title=_get_type_schema_name(field_type),
@@ -609,7 +610,7 @@ def create_schema_for_typed_dict(
                 plugins=plugins,
                 schemas=schemas,
             )
-            for k, v in field_type.__annotations__.items()
+            for k, v in get_type_hints(field_type, include_extras=False).items()
         },
         type=OpenAPIType.OBJECT,
         title=_get_type_schema_name(field_type),


Test patch:
diff --git a/tests/openapi/test_schema.py b/tests/openapi/test_schema.py
--- a/tests/openapi/test_schema.py
+++ b/tests/openapi/test_schema.py
@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 from enum import Enum
-from typing import Dict, Literal
+from typing import TYPE_CHECKING, Dict, Literal
 
 import pytest
 from pydantic import BaseModel
@@ -11,6 +11,9 @@
     _process_schema_result,
     create_schema,
     create_schema_for_annotation,
+    create_schema_for_dataclass,
+    create_schema_for_pydantic_model,
+    create_schema_for_typed_dict,
 )
 from starlite._signature.models import PydanticSignatureModel, SignatureField
 from starlite.app import DEFAULT_OPENAPI_CONFIG
@@ -24,6 +27,10 @@
 from starlite.testing import create_test_client
 from tests import Person, Pet
 
+if TYPE_CHECKING:
+    from types import ModuleType
+    from typing import Callable
+
 
 def test_process_schema_result() -> None:
     test_str = "abc"
@@ -177,3 +184,63 @@ def test_title_validation() -> None:
             plugins=[],
             schemas=schemas,
         )
+
+
+@pytest.mark.parametrize("with_future_annotations", [True, False])
+def test_create_schema_for_pydantic_model_with_annotated_model_attribute(
+    with_future_annotations: bool, create_module: "Callable[[str], ModuleType]"
+) -> None:
+    """Test that a model with an annotated attribute is correctly handled."""
+    module = create_module(
+        f"""
+{'from __future__ import annotations' if with_future_annotations else ''}
+from typing_extensions import Annotated
+from pydantic import BaseModel
+
+class Foo(BaseModel):
+    foo: Annotated[int, "Foo description"]
+"""
+    )
+    schema = create_schema_for_pydantic_model(module.Foo, generate_examples=False, plugins=[], schemas={})
+    assert schema.properties and "foo" in schema.properties
+
+
+@pytest.mark.parametrize("with_future_annotations", [True, False])
+def test_create_schema_for_dataclass_with_annotated_model_attribute(
+    with_future_annotations: bool, create_module: "Callable[[str], ModuleType]"
+) -> None:
+    """Test that a model with an annotated attribute is correctly handled."""
+    module = create_module(
+        f"""
+{'from __future__ import annotations' if with_future_annotations else ''}
+from typing_extensions import Annotated
+from dataclasses import dataclass
+
+@dataclass
+class Foo:
+    foo: Annotated[int, "Foo description"]
+"""
+    )
+    schema = create_schema_for_dataclass(module.Foo, generate_examples=False, plugins=[], schemas={})
+    assert schema.properties and "foo" in schema.properties
+
+
+@pytest.mark.parametrize("with_future_annotations", [True, False])
+def test_create_schema_for_typedict_with_annotated_required_and_not_required_model_attributes(
+    with_future_annotations: bool, create_module: "Callable[[str], ModuleType]"
+) -> None:
+    """Test that a model with an annotated attribute is correctly handled."""
+    module = create_module(
+        f"""
+{'from __future__ import annotations' if with_future_annotations else ''}
+from typing_extensions import Annotated, Required, NotRequired
+from typing import TypedDict
+
+class Foo(TypedDict):
+    foo: Annotated[int, "Foo description"]
+    bar: Annotated[Required[int], "Bar description"]
+    baz: Annotated[NotRequired[int], "Baz description"]
+"""
+    )
+    schema = create_schema_for_typed_dict(module.Foo, generate_examples=False, plugins=[], schemas={})
+    assert schema.properties and all(key in schema.properties for key in ("foo", "bar", "baz"))