Repository: litestar-org/litestar

Issue:
Bug: Schema generation partially broken since litestar version 2.3.0
### Description

2.2.1 is my last working version of litestar. 

Before:
<img width="467" alt="image" src="https://github.com/litestar-org/litestar/assets/85191795/dc9594b1-4b09-4607-9061-dcd65bf0a09f">

After:
I first get this `internal server error` when i first try to go to my Swagger URL

<img width="436" alt="image" src="https://github.com/litestar-org/litestar/assets/85191795/90112884-907e-4ee0-a14c-a92c338ef761">

And then when i refresh once more, it goes to my swagger page, but only 2/3 of it.



<img width="217" alt="image" src="https://github.com/litestar-org/litestar/assets/85191795/74f16208-e80a-46de-b580-3dd566e0f14b">

With no changes in my code, the problems just start at version 2.3.0 and beyond. Just wanted to bring attention to this, as I will now be sticking to litestar 2.2.1 until this is resolved.

### URL to code causing the issue

_No response_

### MCVE

```python
How my app code looks like when passing in my controllers:

app = Litestar(
    route_handlers=[
        read_root,
        refresh_templates,
        LinuxPXEController,
        WindowsPXEController,
        ESXiPXEController
    ],
 ...
```


### Steps to reproduce

_No response_

### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

_No response_

### Litestar Version

2.3.0

### Platform

- [X] 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/2635">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2635/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2635/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
@euthuppan could you please enable `debug` mode on the application and let us know if that gives you additional output?

```python
app = Litestar(
    debug=True,
    route_handlers=[
        read_root,
        refresh_templates,
        LinuxPXEController,
        WindowsPXEController,
        ESXiPXEController
    ],
    ```
I've also started running into this issue with v2.3, find a screenshot of the error in debug mode below:
![Screenshot 2023-11-09 at 11 39 54](https://github.com/litestar-org/litestar/assets/20231751/6eb6b953-781c-4f86-8d96-a947ff92128c)


This also happens for ReDoc and Elements, so it's consistent across all OpenAPI renderers.
@timwedde @euthuppan could one of you provide an MCVE that reproduces this?

Also, which Pydantic version are you using?
Took me a little while to track this down, but here you go:
```py
# pip install pydantic==2.4.2 litestar==2.3.2

from litestar import Litestar, get
from pydantic import BaseModel, Field


class Model(BaseModel):
    id: int
    fail: dict = {}  # fails
    # fail: dict = Field(default={})  # fails
    # fail: dict = Field(default_factory=dict)  # works


@get("/")
async def mcve() -> Model:
    return Model(id=1)


app = Litestar(
    debug=True,
    route_handlers=[mcve],
)
```

Seems that this may now be semi-intended behavior since the `default_factory` approach is likely the objectively correct one anyway, but it does somewhat smell like a breaking change in a minor version update. For reference, this does work properly on Litestar versions before 2.3.x, with the same pydantic version.
So things seems to have broken on 46ea14bf045f1804e0a1623407ec525179acbfb4 (#2553) for the MCVE above (`fail: dict = {}`). Bisected all the way from a7fe0f306e541253351fb1ad373bf6ff590eef9a (v2.2.0) to 0c6a57f8454838572aa6eaffd89123c778d638df (current main)
So change this https://github.com/litestar-org/litestar/blob/0c6a57f8454838572aa6eaffd89123c778d638df/litestar/contrib/pydantic/utils.py#L164-L165

```py
def is_pydantic_undefined(value: Any) -> bool:
    return any(v is value for v in PYDANTIC_UNDEFINED_SENTINELS)
```

so that it is the same as this (avoid the hash)?

https://github.com/litestar-org/litestar/blob/0c6a57f8454838572aa6eaffd89123c778d638df/litestar/utils/predicates.py#L314-L323


Thanks for the effort @Alc-Alc! The proposed change looks good to me, do you want to implement it (and add a test case to reproduce)?
Also thanks @timwedde for the great mcve! This is really helpful! :)

Resolving patch:
diff --git a/litestar/contrib/pydantic/utils.py b/litestar/contrib/pydantic/utils.py
--- a/litestar/contrib/pydantic/utils.py
+++ b/litestar/contrib/pydantic/utils.py
@@ -162,4 +162,4 @@ def is_pydantic_2_model(
 
 
 def is_pydantic_undefined(value: Any) -> bool:
-    return value in PYDANTIC_UNDEFINED_SENTINELS
+    return any(v is value for v in PYDANTIC_UNDEFINED_SENTINELS)


Test patch:
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
@@ -547,3 +547,35 @@ class Foo(BaseModel):
     )
     schema = schemas["Foo"]
     assert schema.properties and "foo" in schema.properties
+
+
+def test_create_schema_for_pydantic_model_with_unhashable_literal_default(
+    create_module: "Callable[[str], ModuleType]",
+) -> None:
+    """Test that a model with unhashable literal defaults is correctly handled."""
+    module = create_module(
+        """
+from pydantic import BaseModel, Field
+
+class Model(BaseModel):
+    id: int
+    dict_default: dict = {}
+    dict_default_in_field: dict = Field(default={})
+    dict_default_in_factory: dict = Field(default_factory=dict)
+    list_default: list = []
+    list_default_in_field: list = Field(default=[])
+    list_default_in_factory: list = Field(default_factory=list)
+"""
+    )
+    schemas: Dict[str, Schema] = {}
+    SchemaCreator(schemas=schemas, plugins=[PydanticSchemaPlugin()]).for_field_definition(
+        FieldDefinition.from_annotation(module.Model)
+    )
+    schema = schemas["Model"]
+    assert schema.properties
+    assert "dict_default" in schema.properties
+    assert "dict_default_in_field" in schema.properties
+    assert "dict_default_in_factory" in schema.properties
+    assert "list_default" in schema.properties
+    assert "list_default_in_field" in schema.properties
+    assert "list_default_in_factory" in schema.properties