Repository: litestar-org/litestar

Issue:
enhancement: OpenAPI re-use existing schemas of same name
### Description

We're getting an unexpected `ImproperlyConfiguredException` in `litestar/_openapi/schema_generation/schema.py", line 594, in process_schema_result`.

In our app, we defined a base`Player` model using Pydantic. This model is returned on its own and also as a subdocument in other endpoints. 

Schema generation fails recognizes two different schemas with the same title. However, the schemas only differ in their description. The description itself is set as a field-level annotation when `Player` is used as a subdocument. 

This makes setting the `description` at the field level much less useful.

I may be missing something. Looking at the code it hashes the schemas to ensure a 1-1 relationship between titles and schemas. I don't believe this was previously a constraint and schemas were produced based on the module path. I may not understand why this is now a requirement.

I've provided an example showing this behavior for both msgspec and Pydantic.

This behavior is new to Litestar 2.0 and did not exist in Litestar 1.51





### URL to code causing the issue

_No response_

### MCVE

```python
### Example showing the issue with Pydantic

import traceback
from litestar import Litestar, get
from litestar.params import Parameter
from pydantic import BaseModel, Field

from litestar.types.asgi_types import Scope


class Player(BaseModel):
    id: str
    name: str
    position: str
    jersey: str


class Play(BaseModel):
    id: str
    ball_carrier: Player = Field(description="Sub document representing player who carried the ball")


@get("/player", sync_to_thread=True)
def get_player_by_id(id: str = Parameter(str)) -> Player:
    return Player(id=id, name="Chris", position="RB", jersey="00")


@get("/play", sync_to_thread=True, include_in_schema=True)
def get_play_by_id(id: str = Parameter(str)) -> Play:
    return Play(id=id, ball_carrier=Player(id="asdasd", name="chris", position="rb", jersey="00"))


async def after_exception_hook(exc: Exception, scope: "Scope") -> None:
    traceback.print_exc()


app = Litestar(route_handlers=[get_player_by_id, get_play_by_id], after_exception=[after_exception_hook])

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app)
```


### Example using msgspec

```py
import traceback
from litestar import Litestar, get
from litestar.params import Parameter

from litestar.types.asgi_types import Scope

from msgspec import Meta, Struct
from typing import Annotated


class Player(Struct):
    id: str
    name: str
    position: str
    jersey: str


class Play(Struct):
    id: str
    ball_carrier: Annotated[Player, Meta(description="Sub document representing player who carried the ball")]


@get("/player", sync_to_thread=True)
def get_player_by_id(id: str = Parameter(str)) -> Player:
    return Player(id=id, name="Chris", position="RB", jersey="00")


@get("/play", sync_to_thread=True, include_in_schema=True)
def get_play_by_id(id: str = Parameter(str)) -> Play:
    return Play(id=id, ball_carrier=Player(id="asdasd", name="chris", position="rb", jersey="00"))


async def after_exception_hook(exc: Exception, scope: "Scope") -> None:
    traceback.print_exc()


app = Litestar(route_handlers=[get_player_by_id, get_play_by_id], after_exception=[after_exception_hook])

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app)

```
```


### Steps to reproduce

```bash
1. Create a base app
2. Define a model and set it as the return type of an endpoint
3. Define a second model, containing a field annotated with the model defined in step 2. Set a `description` on the field (i.e. Pydantic `Field(description="{desc}"` or msgspec  Annotated[Player, Meta(description="{desc}")]). Create a second endpoint returning this model
4. See error
```


### Screenshots

_No response_

### Logs

```bash
litestar.exceptions.http_exceptions.ImproperlyConfiguredException: 500: Two different schemas with the title Player have been defined.

first: {"properties":{"id":{"type":"string"},"name":{"type":"string"},"position":{"type":"string"},"jersey":{"type":"string"}},"type":"object","required":["id","jersey","name","position"],"title":"Player"}
second: {"properties":{"id":{"type":"string"},"name":{"type":"string"},"position":{"type":"string"},"jersey":{"type":"string"}},"type":"object","required":["id","jersey","name","position"],"title":"Player","description":"Sub document representing player who carried the ball"}
```


### Litestar Version

Python 3.11
litestar[full] @ 2.2.1
pydantic @ 2.4.2


### Platform

- [ ] Linux
- [X] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh Litestar dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/2471">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2471/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2471/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
After poking around for a bit I guess I'm somewhat confused as to why we care so much about the title being unique.

In `litestar._openapi.schema_generation.schema`.

Revising this:
```python
 if schema.title and schema.type in (OpenAPIType.OBJECT, OpenAPIType.ARRAY):
            if schema.title in self.schemas and hash(self.schemas[schema.title]) != hash(schema):
                raise ImproperlyConfiguredException(
                    f"Two different schemas with the title {schema.title} have been defined.\n\n"
                    f"first: {encode_json(self.schemas[schema.title].to_schema()).decode()}\n"
                    f"second: {encode_json(schema.to_schema()).decode()}\n\n"
                    f"To fix this issue, either rename the base classes from which these titles are derived or manually"
                    f"set a 'title' kwarg in the route handler."
                )

            self.schemas[schema.title] = schema
            return Reference(ref=f"#/components/schemas/{schema.title}")
 return schema
```

To this:
```python
if schema.title and schema.type in (OpenAPIType.OBJECT, OpenAPIType.ARRAY):
            class_name = str(get_text_in_single_quotes(str(field.annotation)).replace(".", ""))

            existing = self.schemas.get(class_name)
            if existing:
                return schema

            self.schemas[name_title] = schema
            return Reference(ref=f"#/components/schemas/{class_name}")
return schema
```

Seems to work. I'm sure I'm missing something here. Open to any input on it.
Thanks for the excellent report! Hopefully someone with more knowledge on this than me can chime in. Git blame is no help.

Do all tests pass with your change?
No, they would need to be updated to reflect the new naming scheme as they're currently hardcoded to the old one.

I'd make a PR for this but I'm afraid I'm not particularly familiar with the OpenAPI spec and I'm not quite sure if this was changed for compliance purposes?

Resolving patch:
diff --git a/litestar/_openapi/schema_generation/schema.py b/litestar/_openapi/schema_generation/schema.py
--- a/litestar/_openapi/schema_generation/schema.py
+++ b/litestar/_openapi/schema_generation/schema.py
@@ -43,12 +43,13 @@
     create_string_constrained_field_schema,
 )
 from litestar._openapi.schema_generation.utils import (
+    _get_normalized_schema_key,
     _should_create_enum_schema,
     _should_create_literal_schema,
     _type_or_first_not_none_inner_type,
     get_formatted_examples,
 )
-from litestar.datastructures.upload_file import UploadFile
+from litestar.datastructures import UploadFile
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.openapi.spec import Reference
 from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType
@@ -56,7 +57,6 @@
 from litestar.pagination import ClassicPagination, CursorPagination, OffsetPagination
 from litestar.params import BodyKwarg, ParameterKwarg
 from litestar.plugins import OpenAPISchemaPlugin
-from litestar.serialization import encode_json
 from litestar.types import Empty
 from litestar.typing import FieldDefinition
 from litestar.utils.helpers import get_name
@@ -652,15 +652,11 @@ def process_schema_result(self, field: FieldDefinition, schema: Schema) -> Schem
             schema.examples = get_formatted_examples(field, create_examples_for_field(field))
 
         if schema.title and schema.type in (OpenAPIType.OBJECT, OpenAPIType.ARRAY):
-            if schema.title in self.schemas and hash(self.schemas[schema.title]) != hash(schema):
-                raise ImproperlyConfiguredException(
-                    f"Two different schemas with the title {schema.title} have been defined.\n\n"
-                    f"first: {encode_json(self.schemas[schema.title].to_schema()).decode()}\n"
-                    f"second: {encode_json(schema.to_schema()).decode()}\n\n"
-                    f"To fix this issue, either rename the base classes from which these titles are derived or manually"
-                    f"set a 'title' kwarg in the route handler."
-                )
+            class_name = _get_normalized_schema_key(str(field.annotation))
+
+            if class_name in self.schemas:
+                return Reference(ref=f"#/components/schemas/{class_name}", description=schema.description)
 
-            self.schemas[schema.title] = schema
-            return Reference(ref=f"#/components/schemas/{schema.title}")
+            self.schemas[class_name] = schema
+            return Reference(ref=f"#/components/schemas/{class_name}")
         return schema
diff --git a/litestar/_openapi/schema_generation/utils.py b/litestar/_openapi/schema_generation/utils.py
--- a/litestar/_openapi/schema_generation/utils.py
+++ b/litestar/_openapi/schema_generation/utils.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import re
 from enum import Enum
 from typing import TYPE_CHECKING, Any, Mapping
 
@@ -15,6 +16,7 @@
     "_type_or_first_not_none_inner_type",
     "_should_create_enum_schema",
     "_should_create_literal_schema",
+    "_get_normalized_schema_key",
 )
 
 
@@ -82,6 +84,25 @@ def _should_create_literal_schema(field_definition: FieldDefinition) -> bool:
     )
 
 
+TYPE_NAME_NORMALIZATION_SUB_REGEX = re.compile(r"[^a-zA-Z0-9]+")
+TYPE_NAME_NORMALIZATION_TRIM_REGEX = re.compile(r"^(_+class_+|_+)|_+$")
+
+
+def _get_normalized_schema_key(type_annotation_str: str) -> str:
+    """Normalize a type annotation, replacing all non-alphanumeric with underscores. Existing underscores will be left as-is
+
+    Args:
+        type_annotation_str (str): A string representing a type annotation (i.e. 'typing.Dict[str, typing.Any]' or '<class 'model.Foo'>')
+
+    Returns:
+        str: A normalized version of the input string
+    """
+    # Use a regular expression to replace non-alphanumeric characters with underscores
+    return re.sub(
+        TYPE_NAME_NORMALIZATION_TRIM_REGEX, "", re.sub(TYPE_NAME_NORMALIZATION_SUB_REGEX, "_", type_annotation_str)
+    )
+
+
 def get_formatted_examples(field_definition: FieldDefinition, examples: Sequence[Example]) -> Mapping[str, Example]:
     """Format the examples into the OpenAPI schema format."""
 


Test patch:
diff --git a/tests/examples/test_openapi.py b/tests/examples/test_openapi.py
--- a/tests/examples/test_openapi.py
+++ b/tests/examples/test_openapi.py
@@ -19,7 +19,11 @@ def test_schema_generation() -> None:
                                 "description": "Request fulfilled, document follows",
                                 "headers": {},
                                 "content": {
-                                    "application/json": {"schema": {"$ref": "#/components/schemas/IdContainer"}}
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/docs_examples_openapi_customize_pydantic_model_name_IdModel"
+                                        }
+                                    }
                                 },
                             }
                         },
@@ -29,7 +33,7 @@ def test_schema_generation() -> None:
             },
             "components": {
                 "schemas": {
-                    "IdContainer": {
+                    "docs_examples_openapi_customize_pydantic_model_name_IdModel": {
                         "properties": {"id": {"type": "string", "format": "uuid", "description": "Any UUID string"}},
                         "type": "object",
                         "required": ["id"],
diff --git a/tests/unit/test_contrib/test_attrs/test_schema_plugin.py b/tests/unit/test_contrib/test_attrs/test_schema_plugin.py
--- a/tests/unit/test_contrib/test_attrs/test_schema_plugin.py
+++ b/tests/unit/test_contrib/test_attrs/test_schema_plugin.py
@@ -5,8 +5,8 @@
 
 from litestar._openapi.schema_generation.schema import (
     SchemaCreator,
-    _get_type_schema_name,
 )
+from litestar._openapi.schema_generation.utils import _get_normalized_schema_key
 from litestar.contrib.attrs.attrs_schema_plugin import AttrsSchemaPlugin
 from litestar.openapi.spec import OpenAPIType
 from litestar.openapi.spec.schema import Schema
@@ -30,7 +30,7 @@ def test_schema_generation_with_generic_classes() -> None:
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas, plugins=[AttrsSchemaPlugin()]).for_field_definition(field_definition)
 
-    name = _get_type_schema_name(field_definition)
+    name = _get_normalized_schema_key(str(field_definition.annotation))
     properties = schemas[name].properties
     expected_foo_schema = Schema(type=OpenAPIType.INTEGER)
     expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)])
diff --git a/tests/unit/test_contrib/test_attrs/test_schema_spec_generation.py b/tests/unit/test_contrib/test_attrs/test_schema_spec_generation.py
--- a/tests/unit/test_contrib/test_attrs/test_schema_spec_generation.py
+++ b/tests/unit/test_contrib/test_attrs/test_schema_spec_generation.py
@@ -24,8 +24,8 @@ def handler(data: Person) -> Person:
     with create_test_client(handler) as client:
         schema = client.app.openapi_schema
         assert schema
-
-        assert schema.to_schema()["components"]["schemas"]["Person"] == {
+        key_name = "tests_unit_test_contrib_test_attrs_test_schema_spec_generation_test_spec_generation_locals_Person"
+        assert schema.to_schema()["components"]["schemas"][key_name] == {
             "properties": {
                 "first_name": {"type": "string"},
                 "last_name": {"type": "string"},
@@ -41,7 +41,10 @@ def handler(data: Person) -> Person:
                 "pets": {
                     "oneOf": [
                         {"type": "null"},
-                        {"items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array"},
+                        {
+                            "items": {"$ref": "#/components/schemas/tests_models_DataclassPet"},
+                            "type": "array",
+                        },
                     ]
                 },
             },
diff --git a/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py b/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py
--- a/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py
+++ b/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py
@@ -96,22 +96,22 @@ def test_piccolo_dto_openapi_spec_generation() -> None:
     post_operation = concert_path.post
     assert (
         post_operation.request_body.content["application/json"].schema.ref  # type: ignore
-        == "#/components/schemas/CreateConcertConcertRequestBody"
+        == "#/components/schemas/litestar_dto_backend_CreateConcertConcertRequestBody"
     )
 
     studio_path_get_operation = studio_path.get
     assert (
         studio_path_get_operation.responses["200"].content["application/json"].schema.ref  # type: ignore
-        == "#/components/schemas/RetrieveStudioRecordingStudioResponseBody"
+        == "#/components/schemas/litestar_dto_backend_RetrieveStudioRecordingStudioResponseBody"
     )
 
     venues_path_get_operation = venues_path.get
     assert (
         venues_path_get_operation.responses["200"].content["application/json"].schema.items.ref  # type: ignore
-        == "#/components/schemas/RetrieveVenuesVenueResponseBody"
+        == "#/components/schemas/litestar_dto_backend_RetrieveVenuesVenueResponseBody"
     )
 
-    concert_schema = schema.components.schemas["CreateConcertConcertRequestBody"]
+    concert_schema = schema.components.schemas["litestar_dto_backend_CreateConcertConcertRequestBody"]
     assert concert_schema
     assert concert_schema.to_schema() == {
         "properties": {
@@ -124,7 +124,7 @@ def test_piccolo_dto_openapi_spec_generation() -> None:
         "type": "object",
     }
 
-    record_studio_schema = schema.components.schemas["RetrieveStudioRecordingStudioResponseBody"]
+    record_studio_schema = schema.components.schemas["litestar_dto_backend_RetrieveStudioRecordingStudioResponseBody"]
     assert record_studio_schema
     assert record_studio_schema.to_schema() == {
         "properties": {
@@ -138,7 +138,7 @@ def test_piccolo_dto_openapi_spec_generation() -> None:
         "type": "object",
     }
 
-    venue_schema = schema.components.schemas["RetrieveVenuesVenueResponseBody"]
+    venue_schema = schema.components.schemas["litestar_dto_backend_RetrieveVenuesVenueResponseBody"]
     assert venue_schema
     assert venue_schema.to_schema() == {
         "properties": {
diff --git a/tests/unit/test_contrib/test_pydantic/test_openapi.py b/tests/unit/test_contrib/test_pydantic/test_openapi.py
--- a/tests/unit/test_contrib/test_pydantic/test_openapi.py
+++ b/tests/unit/test_contrib/test_pydantic/test_openapi.py
@@ -15,6 +15,7 @@
     create_string_constrained_field_schema,
 )
 from litestar._openapi.schema_generation.schema import SchemaCreator
+from litestar._openapi.schema_generation.utils import _get_normalized_schema_key
 from litestar.contrib.pydantic import PydanticPlugin, PydanticSchemaPlugin
 from litestar.openapi import OpenAPIConfig
 from litestar.openapi.spec import Example, Schema
@@ -319,8 +320,9 @@ def handler(data: cls) -> cls:
     with create_test_client(handler) as client:
         schema = client.app.openapi_schema
         assert schema
+        key_name = _get_normalized_schema_key(str(cls))
 
-        assert schema.to_schema()["components"]["schemas"][cls.__name__] == {
+        assert schema.to_schema()["components"]["schemas"][key_name] == {
             "properties": {
                 "first_name": {"type": "string"},
                 "last_name": {"type": "string"},
@@ -336,7 +338,10 @@ def handler(data: cls) -> cls:
                 "pets": {
                     "oneOf": [
                         {"type": "null"},
-                        {"items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array"},
+                        {
+                            "items": {"$ref": "#/components/schemas/tests_models_DataclassPet"},
+                            "type": "array",
+                        },
                     ]
                 },
             },
@@ -373,8 +378,9 @@ async def example_route() -> Lookup:
         signature_namespace={"Lookup": Lookup},
     ) as client:
         response = client.get("/schema/openapi.json")
+        key_name = "tests_unit_test_contrib_test_pydantic_test_openapi_test_schema_generation_v1_locals_Lookup"
         assert response.status_code == HTTP_200_OK
-        assert response.json()["components"]["schemas"]["Lookup"]["properties"]["id"] == {
+        assert response.json()["components"]["schemas"][key_name]["properties"]["id"] == {
             "description": "A unique identifier",
             "examples": {"id-example-1": {"value": "e4eaaaf2-d142-11e1-b3e4-080027620cdd"}},
             "maxLength": 16,
@@ -411,7 +417,8 @@ async def example_route() -> Lookup:
     ) as client:
         response = client.get("/schema/openapi.json")
         assert response.status_code == HTTP_200_OK
-        assert response.json()["components"]["schemas"]["Lookup"]["properties"]["id"] == {
+        key_name = "tests_unit_test_contrib_test_pydantic_test_openapi_test_schema_generation_v2_locals_Lookup"
+        assert response.json()["components"]["schemas"][key_name]["properties"]["id"] == {
             "description": "A unique identifier",
             "examples": {"id-example-1": {"value": "e4eaaaf2-d142-11e1-b3e4-080027620cdd"}},
             "maxLength": 16,
@@ -436,14 +443,18 @@ def handler(data: RequestWithAlias) -> ResponseWithAlias:
     assert app.openapi_schema
     schemas = app.openapi_schema.to_schema()["components"]["schemas"]
     request_key = "second"
-    assert schemas["RequestWithAlias"] == {
+    assert schemas[
+        "tests_unit_test_contrib_test_pydantic_test_openapi_test_schema_by_alias_locals_RequestWithAlias"
+    ] == {
         "properties": {request_key: {"type": "string"}},
         "type": "object",
         "required": [request_key],
         "title": "RequestWithAlias",
     }
     response_key = "first"
-    assert schemas["ResponseWithAlias"] == {
+    assert schemas[
+        "tests_unit_test_contrib_test_pydantic_test_openapi_test_schema_by_alias_locals_ResponseWithAlias"
+    ] == {
         "properties": {response_key: {"type": "string"}},
         "type": "object",
         "required": [response_key],
@@ -471,18 +482,21 @@ def handler(data: RequestWithAlias) -> ResponseWithAlias:
         openapi_config=OpenAPIConfig(title="my title", version="1.0.0"),
         plugins=[PydanticPlugin(prefer_alias=True)],
     )
-
     assert app.openapi_schema
     schemas = app.openapi_schema.to_schema()["components"]["schemas"]
     request_key = "second"
-    assert schemas["RequestWithAlias"] == {
+    assert schemas[
+        "tests_unit_test_contrib_test_pydantic_test_openapi_test_schema_by_alias_plugin_override_locals_RequestWithAlias"
+    ] == {
         "properties": {request_key: {"type": "string"}},
         "type": "object",
         "required": [request_key],
         "title": "RequestWithAlias",
     }
     response_key = "second"
-    assert schemas["ResponseWithAlias"] == {
+    assert schemas[
+        "tests_unit_test_contrib_test_pydantic_test_openapi_test_schema_by_alias_plugin_override_locals_ResponseWithAlias"
+    ] == {
         "properties": {response_key: {"type": "string"}},
         "type": "object",
         "required": [response_key],
@@ -506,7 +520,7 @@ class Model(pydantic_v1.BaseModel):
     schemas: Dict[str, Schema] = {}
     field_definition = FieldDefinition.from_kwarg(name="Model", annotation=Model)
     SchemaCreator(schemas=schemas, plugins=[PydanticSchemaPlugin()]).for_field_definition(field_definition)
-    schema = schemas["Model"]
+    schema = schemas["tests_unit_test_contrib_test_pydantic_test_openapi_test_create_schema_for_field_v1_locals_Model"]
 
     assert schema.properties
 
@@ -527,7 +541,7 @@ class Model(pydantic_v2.BaseModel):
     schemas: Dict[str, Schema] = {}
     field_definition = FieldDefinition.from_kwarg(name="Model", annotation=Model)
     SchemaCreator(schemas=schemas, plugins=[PydanticSchemaPlugin()]).for_field_definition(field_definition)
-    schema = schemas["Model"]
+    schema = schemas["tests_unit_test_contrib_test_pydantic_test_openapi_test_create_schema_for_field_v2_locals_Model"]
 
     assert schema.properties
 
@@ -558,7 +572,8 @@ class Foo(BaseModel):
     SchemaCreator(schemas=schemas, plugins=[PydanticSchemaPlugin()]).for_field_definition(
         FieldDefinition.from_annotation(module.Foo)
     )
-    schema = schemas["Foo"]
+    key_name = _get_normalized_schema_key(str(module.Foo))
+    schema = schemas[key_name]
     assert schema.properties and "foo" in schema.properties
 
 
@@ -584,7 +599,7 @@ class Model(BaseModel):
     SchemaCreator(schemas=schemas, plugins=[PydanticSchemaPlugin()]).for_field_definition(
         FieldDefinition.from_annotation(module.Model)
     )
-    schema = schemas["Model"]
+    schema = schemas[f"{module.__name__}_Model"]
     assert schema.properties
     assert "dict_default" in schema.properties
     assert "dict_default_in_field" in schema.properties
diff --git a/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py b/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py
--- a/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py
+++ b/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py
@@ -10,8 +10,8 @@
 
 from litestar._openapi.schema_generation.schema import (
     SchemaCreator,
-    _get_type_schema_name,
 )
+from litestar._openapi.schema_generation.utils import _get_normalized_schema_key
 from litestar.contrib.pydantic.pydantic_schema_plugin import PydanticSchemaPlugin
 from litestar.openapi.spec import OpenAPIType
 from litestar.openapi.spec.schema import Schema
@@ -41,7 +41,7 @@ def test_schema_generation_with_generic_classes(model: Type[Union[PydanticV1Gene
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas, plugins=[PydanticSchemaPlugin()]).for_field_definition(field_definition)
 
-    name = _get_type_schema_name(field_definition)
+    name = _get_normalized_schema_key(str(field_definition.annotation))
     properties = schemas[name].properties
     expected_foo_schema = Schema(type=OpenAPIType.INTEGER)
     expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)])
diff --git a/tests/unit/test_openapi/test_integration.py b/tests/unit/test_openapi/test_integration.py
--- a/tests/unit/test_openapi/test_integration.py
+++ b/tests/unit/test_openapi/test_integration.py
@@ -169,7 +169,9 @@ async def example_route() -> Lookup:
     ) as client:
         response = client.get("/schema/openapi.json")
         assert response.status_code == HTTP_200_OK
-        assert response.json()["components"]["schemas"]["Lookup"]["properties"]["id"] == {
+        assert response.json()["components"]["schemas"][
+            "tests_unit_test_openapi_test_integration_test_msgspec_schema_generation_locals_Lookup"
+        ]["properties"]["id"] == {
             "description": "A unique identifier",
             "examples": {"id-example-1": {"value": "e4eaaaf2-d142-11e1-b3e4-080027620cdd"}},
             "maxLength": 16,
@@ -225,7 +227,6 @@ def handler_foo_int() -> Foo[int]:
         ),
     ) as client:
         response = client.get("/schema/openapi.json")
-
         assert response.status_code == HTTP_200_OK
         assert response.json() == {
             "info": {"title": "Example API", "version": "1.0.0"},
@@ -240,7 +241,13 @@ def handler_foo_int() -> Foo[int]:
                             "200": {
                                 "description": "Request fulfilled, document follows",
                                 "headers": {},
-                                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo[str]"}}},
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/tests_unit_test_openapi_test_integration_Foo_str"
+                                        }
+                                    }
+                                },
                             }
                         },
                         "deprecated": False,
@@ -254,7 +261,13 @@ def handler_foo_int() -> Foo[int]:
                             "200": {
                                 "description": "Request fulfilled, document follows",
                                 "headers": {},
-                                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo[int]"}}},
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/tests_unit_test_openapi_test_integration_Foo_int"
+                                        }
+                                    }
+                                },
                             }
                         },
                         "deprecated": False,
@@ -263,13 +276,13 @@ def handler_foo_int() -> Foo[int]:
             },
             "components": {
                 "schemas": {
-                    "Foo[str]": {
+                    "tests_unit_test_openapi_test_integration_Foo_str": {
                         "properties": {"foo": {"type": "string"}},
                         "type": "object",
                         "required": ["foo"],
                         "title": "Foo[str]",
                     },
-                    "Foo[int]": {
+                    "tests_unit_test_openapi_test_integration_Foo_int": {
                         "properties": {"foo": {"type": "integer"}},
                         "type": "object",
                         "required": ["foo"],
diff --git a/tests/unit/test_openapi/test_request_body.py b/tests/unit/test_openapi/test_request_body.py
--- a/tests/unit/test_openapi/test_request_body.py
+++ b/tests/unit/test_openapi/test_request_body.py
@@ -62,9 +62,10 @@ async def handle_file_list_upload(
         "items": {"type": "string", "contentMediaType": "application/octet-stream"},
         "type": "array",
     }
+
     assert components == {
         "schemas": {
-            "FormData": {
+            "tests_unit_test_openapi_test_request_body_FormData": {
                 "properties": {
                     "cv": {
                         "type": "string",
diff --git a/tests/unit/test_openapi/test_responses.py b/tests/unit/test_openapi/test_responses.py
--- a/tests/unit/test_openapi/test_responses.py
+++ b/tests/unit/test_openapi/test_responses.py
@@ -226,7 +226,8 @@ def handler() -> Response[DataclassPerson]:
     assert isinstance(reference, Reference)
     key = reference.ref.split("/")[-1]
     assert isinstance(schemas[key], Schema)
-    assert key == DataclassPerson.__name__
+
+    assert key == "tests_models_DataclassPerson"
 
 
 def test_create_success_response_with_stream() -> None:
diff --git a/tests/unit/test_openapi/test_schema.py b/tests/unit/test_openapi/test_schema.py
--- a/tests/unit/test_openapi/test_schema.py
+++ b/tests/unit/test_openapi/test_schema.py
@@ -27,18 +27,16 @@
 from litestar._openapi.schema_generation.schema import (
     KWARG_DEFINITION_ATTRIBUTE_TO_OPENAPI_PROPERTY_MAP,
     SchemaCreator,
-    _get_type_schema_name,
 )
-from litestar._openapi.schema_generation.utils import _type_or_first_not_none_inner_type
+from litestar._openapi.schema_generation.utils import _get_normalized_schema_key, _type_or_first_not_none_inner_type
 from litestar.app import DEFAULT_OPENAPI_CONFIG
 from litestar.di import Provide
 from litestar.enums import ParamType
-from litestar.exceptions import ImproperlyConfiguredException
 from litestar.openapi.spec import ExternalDocumentation, OpenAPIType, Reference
 from litestar.openapi.spec.example import Example
 from litestar.openapi.spec.schema import Schema
 from litestar.pagination import ClassicPagination, CursorPagination, OffsetPagination
-from litestar.params import BodyKwarg, Parameter, ParameterKwarg
+from litestar.params import Parameter, ParameterKwarg
 from litestar.testing import create_test_client
 from litestar.types.builtin_types import NoneType
 from litestar.typing import FieldDefinition
@@ -87,6 +85,47 @@ def test_process_schema_result() -> None:
             assert getattr(schema, schema_key) == getattr(kwarg_definition, signature_key)
 
 
+def test_get_normalized_schema_key() -> None:
+    class LocalClass(msgspec.Struct):
+        id: str
+
+    assert (
+        "tests_unit_test_openapi_test_schema_test_get_normalized_schema_key_locals_LocalClass"
+        == _get_normalized_schema_key(str(LocalClass))
+    )
+
+    assert "tests_models_DataclassPerson" == _get_normalized_schema_key(str(DataclassPerson))
+
+    builtin_dict = Dict[str, List[int]]
+    assert "typing_Dict_str_typing_List_int" == _get_normalized_schema_key(str(builtin_dict))
+
+    builtin_with_custom = Dict[str, DataclassPerson]
+    assert "typing_Dict_str_tests_models_DataclassPerson" == _get_normalized_schema_key(str(builtin_with_custom))
+
+    class LocalGeneric(Generic[T]):
+        pass
+
+    assert (
+        "tests_unit_test_openapi_test_schema_test_get_normalized_schema_key_locals_LocalGeneric"
+        == _get_normalized_schema_key(str(LocalGeneric))
+    )
+
+    generic_int = LocalGeneric[int]
+    generic_str = LocalGeneric[str]
+
+    assert (
+        "tests_unit_test_openapi_test_schema_test_get_normalized_schema_key_locals_LocalGeneric_int"
+        == _get_normalized_schema_key(str(generic_int))
+    )
+
+    assert (
+        "tests_unit_test_openapi_test_schema_test_get_normalized_schema_key_locals_LocalGeneric_str"
+        == _get_normalized_schema_key(str(generic_str))
+    )
+
+    assert _get_normalized_schema_key(str(generic_int)) != _get_normalized_schema_key(str(generic_str))
+
+
 def test_dependency_schema_generation() -> None:
     async def top_dependency(query_param: int) -> int:
         return query_param
@@ -153,7 +192,7 @@ class DataclassWithLiteral:
     )
     assert isinstance(result, Reference)
 
-    schema = schemas["DataclassWithLiteral"]
+    schema = schemas["tests_unit_test_openapi_test_schema_test_handling_of_literals_locals_DataclassWithLiteral"]
     assert isinstance(schema, Schema)
     assert schema.properties
 
@@ -187,17 +226,10 @@ def test_title_validation() -> None:
     schema_creator = SchemaCreator(schemas=schemas)
 
     schema_creator.for_field_definition(FieldDefinition.from_kwarg(name="Person", annotation=DataclassPerson))
-    assert schemas.get("DataclassPerson")
+    assert schemas.get("tests_models_DataclassPerson")
 
     schema_creator.for_field_definition(FieldDefinition.from_kwarg(name="Pet", annotation=DataclassPet))
-    assert schemas.get("DataclassPet")
-
-    with pytest.raises(ImproperlyConfiguredException):
-        schema_creator.for_field_definition(
-            FieldDefinition.from_kwarg(
-                name="DataclassPerson", annotation=DataclassPet, kwarg_definition=BodyKwarg(title="DataclassPerson")
-            )
-        )
+    assert schemas.get("tests_models_DataclassPet")
 
 
 @pytest.mark.parametrize("with_future_annotations", [True, False])
@@ -218,7 +250,8 @@ class Foo:
     )
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas).for_field_definition(FieldDefinition.from_annotation(module.Foo))
-    schema = schemas["Foo"]
+    schema_key = _get_normalized_schema_key(str(module.Foo))
+    schema = schemas[schema_key]
     assert schema.properties and "foo" in schema.properties
 
 
@@ -241,7 +274,8 @@ class Foo(TypedDict):
     )
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas).for_field_definition(FieldDefinition.from_annotation(module.Foo))
-    schema = schemas["Foo"]
+    schema_key = _get_normalized_schema_key(str(module.Foo))
+    schema = schemas[schema_key]
     assert schema.properties and all(key in schema.properties for key in ("foo", "bar", "baz"))
 
 
@@ -251,7 +285,7 @@ class Lookup(msgspec.Struct):
 
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas).for_field_definition(FieldDefinition.from_kwarg(name="Lookup", annotation=Lookup))
-    schema = schemas["Lookup"]
+    schema = schemas["tests_unit_test_openapi_test_schema_test_create_schema_from_msgspec_annotated_type_locals_Lookup"]
 
     assert schema.properties["id"].type == OpenAPIType.STRING  # type: ignore
     assert schema.properties["id"].examples == {"id-example-1": Example(value="example")}  # type: ignore
@@ -279,7 +313,7 @@ class MyDataclass:
     SchemaCreator(schemas=schemas).for_field_definition(
         FieldDefinition.from_kwarg(name="MyDataclass", annotation=MyDataclass)
     )
-    schema = schemas["MyDataclass"]
+    schema = schemas["tests_unit_test_openapi_test_schema_test_annotated_types_locals_MyDataclass"]
 
     assert schema.properties["constrained_int"].exclusive_minimum == 1  # type: ignore
     assert schema.properties["constrained_int"].exclusive_maximum == 10  # type: ignore
@@ -337,8 +371,8 @@ def test_schema_generation_with_generic_classes(cls: Any) -> None:
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas).for_field_definition(field_definition)
 
-    name = _get_type_schema_name(field_definition)
-    properties = schemas[name].properties
+    path_name = _get_normalized_schema_key(str(cls))
+    properties = schemas[path_name].properties
     expected_foo_schema = Schema(type=OpenAPIType.INTEGER)
     expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)])
 
@@ -368,8 +402,7 @@ def test_schema_generation_with_generic_classes_constrained() -> None:
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas).for_field_definition(field_definition)
 
-    name = _get_type_schema_name(field_definition)
-    properties = schemas[name].properties
+    properties = schemas["tests_unit_test_openapi_test_schema_ConstrainedGenericDataclass"].properties
 
     assert properties
     assert properties["bound"] == Schema(type=OpenAPIType.INTEGER)
@@ -397,8 +430,8 @@ def test_schema_generation_with_pagination(annotation: Any) -> None:
     field_definition = FieldDefinition.from_annotation(annotation)
     schemas: Dict[str, Schema] = {}
     SchemaCreator(schemas=schemas).for_field_definition(field_definition)
-    name = _get_type_schema_name(field_definition.inner_types[-1])
-    properties = schemas[name].properties
+    schema_key = _get_normalized_schema_key(str(field_definition.inner_types[-1].annotation))
+    properties = schemas[str(schema_key)].properties
 
     expected_foo_schema = Schema(type=OpenAPIType.INTEGER)
     expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.NULL), Schema(type=OpenAPIType.INTEGER)])
diff --git a/tests/unit/test_openapi/test_spec_generation.py b/tests/unit/test_openapi/test_spec_generation.py
--- a/tests/unit/test_openapi/test_spec_generation.py
+++ b/tests/unit/test_openapi/test_spec_generation.py
@@ -4,6 +4,7 @@
 from msgspec import Struct
 
 from litestar import post
+from litestar._openapi.schema_generation.utils import _get_normalized_schema_key
 from litestar.testing import create_test_client
 from tests.models import DataclassPerson, MsgSpecStructPerson, TypedDictPerson
 
@@ -17,8 +18,9 @@ def handler(data: cls) -> cls:
     with create_test_client(handler) as client:
         schema = client.app.openapi_schema
         assert schema
+        schema_key = _get_normalized_schema_key(str(cls))
 
-        assert schema.to_schema()["components"]["schemas"][cls.__name__] == {
+        assert schema.to_schema()["components"]["schemas"][schema_key] == {
             "properties": {
                 "first_name": {"type": "string"},
                 "last_name": {"type": "string"},
@@ -34,7 +36,10 @@ def handler(data: cls) -> cls:
                 "pets": {
                     "oneOf": [
                         {"type": "null"},
-                        {"items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array"},
+                        {
+                            "items": {"$ref": "#/components/schemas/tests_models_DataclassPet"},
+                            "type": "array",
+                        },
                     ]
                 },
             },
@@ -57,7 +62,9 @@ def handler(data: CamelizedStruct) -> CamelizedStruct:
         schema = client.app.openapi_schema
         assert schema
 
-        assert schema.to_schema()["components"]["schemas"][CamelizedStruct.__name__] == {
+        assert schema.to_schema()["components"]["schemas"][
+            "tests_unit_test_openapi_test_spec_generation_test_msgspec_schema_locals_CamelizedStruct"
+        ] == {
             "properties": {"fieldOne": {"type": "integer"}, "fieldTwo": {"type": "number"}},
             "required": ["fieldOne", "fieldTwo"],
             "title": "CamelizedStruct",
diff --git a/tests/unit/test_pagination.py b/tests/unit/test_pagination.py
--- a/tests/unit/test_pagination.py
+++ b/tests/unit/test_pagination.py
@@ -120,7 +120,10 @@ def sync_handler(page_size: int, current_page: int) -> ClassicPagination[Datacla
         assert spec == {
             "schema": {
                 "properties": {
-                    "items": {"items": {"$ref": "#/components/schemas/DataclassPerson"}, "type": "array"},
+                    "items": {
+                        "items": {"$ref": "#/components/schemas/tests_models_DataclassPerson"},
+                        "type": "array",
+                    },
                     "page_size": {"type": "integer", "description": "Number of items per page."},
                     "current_page": {"type": "integer", "description": "Current page number."},
                     "total_pages": {"type": "integer", "description": "Total number of pages."},
@@ -174,7 +177,10 @@ def sync_handler(limit: int, offset: int) -> OffsetPagination[DataclassPerson]:
         assert spec == {
             "schema": {
                 "properties": {
-                    "items": {"items": {"$ref": "#/components/schemas/DataclassPerson"}, "type": "array"},
+                    "items": {
+                        "items": {"$ref": "#/components/schemas/tests_models_DataclassPerson"},
+                        "type": "array",
+                    },
                     "limit": {"type": "integer", "description": "Maximal number of items to send."},
                     "offset": {"type": "integer", "description": "Offset from the beginning of the query."},
                     "total": {"type": "integer", "description": "Total number of items."},
@@ -251,7 +257,10 @@ def sync_handler(cursor: Optional[str] = None) -> CursorPagination[str, Dataclas
         assert spec == {
             "schema": {
                 "properties": {
-                    "items": {"items": {"$ref": "#/components/schemas/DataclassPerson"}, "type": "array"},
+                    "items": {
+                        "items": {"$ref": "#/components/schemas/tests_models_DataclassPerson"},
+                        "type": "array",
+                    },
                     "cursor": {
                         "type": "string",
                         "description": "Unique ID, designating the last identifier in the given data set. This value can be used to request the 'next' batch of records.",