Repository: litestar-org/litestar

Issue:
Bug: SerializationException when running modeling-and-features demo from docs
### Description

Hi,

First of all thanks for developing Litestar, it proves to be a very useful piece of software here. Unfortunately I ran into an issue. 

I ran into an `msgspec_error` when requesting a page backed by sqlalchemy models which are connected via relationships. It seems that the database is correctly queried, a list of objects are returned, but then an exception is thrown when converting the objects to JSON.

I ran into this issue on my production code but when isolating an MCVE I noticed that the provided example in the documentation also shows the same unexpected behaviour on tested on two different machines.  One crucial change to the code is however adding an author to the database.

Since this is quite a show-stopper for me: Thanks in advance for having a look at this!

### URL to code causing the issue

https://docs.litestar.dev/2/tutorials/repository-tutorial/01-modeling-and-features.html

### MCVE

```python
from datetime import date
from typing import TYPE_CHECKING
from uuid import UUID

from sqlalchemy import ForeignKey, select
from sqlalchemy.orm import Mapped, mapped_column, relationship

from litestar import Litestar, get
from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase
from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession


# the SQLAlchemy base includes a declarative model for you to use in your models.
# The `Base` class includes a `UUID` based primary key (`id`)
class Author(UUIDBase):
    name: Mapped[str]
    dob: Mapped[date]
    books: Mapped[list["Book"]] = relationship(back_populates="author", lazy="selectin")


# The `AuditBase` class includes the same UUID` based primary key (`id`) and 2
# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the
# record created, and `updated_at` is the last time the record was modified.
class Book(UUIDAuditBase):
    title: Mapped[str]
    author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id"))
    author: Mapped[Author] = relationship(lazy="joined", innerjoin=True, viewonly=True)


session_config = AsyncSessionConfig(expire_on_commit=False)
sqlalchemy_config = SQLAlchemyAsyncConfig(
    connection_string="sqlite+aiosqlite:///test.sqlite", session_config=session_config
)  # Create 'async_session' dependency.
sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config)


async def on_startup() -> None:
    """Initializes the database."""
    async with sqlalchemy_config.get_engine().begin() as conn:
        await conn.run_sync(UUIDBase.metadata.create_all)
        
    #crucially there needs to be an author in the table for the error to appear
    await conn.execute(Author.__table__.insert().values(name="F. Scott Fitzgerald"))


@get(path="/authors")
async def get_authors(db_session: "AsyncSession", db_engine: "AsyncEngine") -> list[Author]:
    """Interact with SQLAlchemy engine and session."""
    return list(await db_session.scalars(select(Author)))


app = Litestar(
    route_handlers=[get_authors],
    on_startup=[on_startup],
    plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)],
    debug=True
)
```


### Steps to reproduce

```bash
1. Go to the https://docs.litestar.dev/2/tutorials/repository-tutorial/01-modeling-and-features.html page
2. Download the code
3. Run the demo with minimal requirements installed and go to http://localhost:8000/authors
4. See the error
```


### Screenshots

_No response_

### Logs

```bash
File "/usr/local/lib/python3.12/site-packages/litestar/serialization/msgspec_hooks.py", line 143, in encode_json
    raise SerializationException(str(msgspec_error)) from msgspec_error
litestar.exceptions.base_exceptions.SerializationException: Unsupported type: <class '__main__.Author'>
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/litestar/serialization/msgspec_hooks.py", line 141, in encode_json
    return msgspec.json.encode(value, enc_hook=serializer) if serializer else _msgspec_json_encoder.encode(value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/serialization/msgspec_hooks.py", line 88, in default_serializer
    raise TypeError(f"Unsupported type: {type(value)!r}")
TypeError: Unsupported type: <class '__main__.Author'>
 
The above exception was the direct cause of the following exception:
 
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/litestar/middleware/exceptions/middleware.py", line 219, in __call__
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/litestar/routes/http.py", line 82, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/routes/http.py", line 134, in _get_response_for_request
    return await self._call_handler_function(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/routes/http.py", line 158, in _call_handler_function
    response: ASGIApp = await route_handler.to_response(app=scope["app"], data=response_data, request=request)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/handlers/http_handlers/base.py", line 557, in to_response
    return await response_handler(app=app, data=data, request=request)  # type: ignore[call-arg]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/handlers/http_handlers/_utils.py", line 79, in handler
    return response.to_asgi_response(app=None, request=request, headers=normalize_headers(headers), cookies=cookies)  # pyright: ignore
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/response/base.py", line 451, in to_asgi_response
    body=self.render(self.content, media_type, get_serializer(type_encoders)),
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/response/base.py", line 392, in render
    return encode_json(content, enc_hook)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/litestar/serialization/msgspec_hooks.py", line 143, in encode_json
    raise SerializationException(str(msgspec_error)) from msgspec_error
litestar.exceptions.base_exceptions.SerializationException: Unsupported type: <class '__main__.Author'>
INFO:     127.0.0.1:44906 - "GET /authors HTTP/1.1" 500 Internal Server Error
```


### Litestar Version

2.8.2

### 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 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/3464">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/3464/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/3464/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
Can you try `SQLAlchemyPlugin` instead of `SQLAlchemyInitPlugin`?
Allright, the following code, using SQLAlchemyPlugin works:

```python
from datetime import date
from typing import TYPE_CHECKING
from uuid import UUID

from sqlalchemy import ForeignKey, select
from sqlalchemy.orm import Mapped, mapped_column, relationship

from litestar import Litestar, get
from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase
from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin

from litestar.testing import create_test_client

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession

# the SQLAlchemy base includes a declarative model for you to use in your models.
# The `Base` class includes a `UUID` based primary key (`id`)
class Author(UUIDBase):
    name: Mapped[str]
    dob: Mapped[date]
    books: Mapped[list["Book"]] = relationship(back_populates="author", lazy="selectin")


# The `AuditBase` class includes the same UUID` based primary key (`id`) and 2
# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the
# record created, and `updated_at` is the last time the record was modified.
class Book(UUIDAuditBase):
    title: Mapped[str]
    author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id"))
    author: Mapped[Author] = relationship(lazy="joined", innerjoin=True, viewonly=True)


session_config = AsyncSessionConfig(expire_on_commit=False)
sqlalchemy_config = SQLAlchemyAsyncConfig(
    connection_string="sqlite+aiosqlite:///testb.sqlite", session_config=session_config
)  # Create 'async_session' dependency.
sqlalchemy_plugin = SQLAlchemyPlugin(config=sqlalchemy_config)


async def on_startup() -> None:
    """Initializes the database."""
    async with sqlalchemy_config.get_engine().begin() as conn:
        await conn.run_sync(UUIDBase.metadata.create_all)
    
    async with sqlalchemy_config.get_session() as session:
        session.add(Author(name="author1",dob=date(1990,1,1),id=UUID("00000000-0000-0000-0000-000000000001") ))
        session.add(Book(title="blaat",author_id=UUID("00000000-0000-0000-0000-000000000001")))
        await session.commit()

@get(path="/authors")
async def get_authors(db_session: "AsyncSession", db_engine: "AsyncEngine") -> list[Author]:
    """Interact with SQLAlchemy engine and session."""
    return list(await db_session.scalars(select(Author)))

app = Litestar(
    route_handlers=[get_authors],
    on_startup=[on_startup],
    plugins=[SQLAlchemyPlugin(config=sqlalchemy_config)],
    debug=True
)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug")
`````

A change in the documentation would be nice 
@JorenSix Feel free to PR if you want to, if not someone else will pick this up as per their availability 

Resolving patch:
diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py
--- a/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py
+++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py
@@ -1,16 +1,17 @@
+from __future__ import annotations
+
+import uuid
 from datetime import date
-from typing import TYPE_CHECKING
+from typing import List
 from uuid import UUID
 
-from sqlalchemy import ForeignKey, select
+from sqlalchemy import ForeignKey, func, select
+from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
 from sqlalchemy.orm import Mapped, mapped_column, relationship
 
 from litestar import Litestar, get
 from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase
-from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin
-
-if TYPE_CHECKING:
-    from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
+from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin
 
 
 # the SQLAlchemy base includes a declarative model for you to use in your models.
@@ -18,7 +19,7 @@
 class Author(UUIDBase):
     name: Mapped[str]
     dob: Mapped[date]
-    books: Mapped[list["Book"]] = relationship(back_populates="author", lazy="selectin")
+    books: Mapped[List[Book]] = relationship(back_populates="author", lazy="selectin")
 
 
 # The `AuditBase` class includes the same UUID` based primary key (`id`) and 2
@@ -32,19 +33,24 @@ class Book(UUIDAuditBase):
 
 session_config = AsyncSessionConfig(expire_on_commit=False)
 sqlalchemy_config = SQLAlchemyAsyncConfig(
-    connection_string="sqlite+aiosqlite:///test.sqlite", session_config=session_config
+    connection_string="sqlite+aiosqlite:///test.sqlite", session_config=session_config, create_all=True
 )  # Create 'async_session' dependency.
-sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config)
 
 
 async def on_startup() -> None:
-    """Initializes the database."""
-    async with sqlalchemy_config.get_engine().begin() as conn:
-        await conn.run_sync(UUIDBase.metadata.create_all)
+    """Adds some dummy data if no data is present."""
+    async with sqlalchemy_config.get_session() as session:
+        statement = select(func.count()).select_from(Author)
+        count = await session.execute(statement)
+        if not count.scalar():
+            author_id = uuid.uuid4()
+            session.add(Author(name="Stephen King", dob=date(1954, 9, 21), id=author_id))
+            session.add(Book(title="It", author_id=author_id))
+            await session.commit()
 
 
 @get(path="/authors")
-async def get_authors(db_session: "AsyncSession", db_engine: "AsyncEngine") -> list[Author]:
+async def get_authors(db_session: AsyncSession, db_engine: AsyncEngine) -> List[Author]:
     """Interact with SQLAlchemy engine and session."""
     return list(await db_session.scalars(select(Author)))
 
@@ -52,5 +58,6 @@ async def get_authors(db_session: "AsyncSession", db_engine: "AsyncEngine") -> l
 app = Litestar(
     route_handlers=[get_authors],
     on_startup=[on_startup],
-    plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)],
+    debug=True,
+    plugins=[SQLAlchemyPlugin(config=sqlalchemy_config)],
 )


Test patch:
diff --git a/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py b/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py
new file mode 100644
--- /dev/null
+++ b/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py
@@ -0,0 +1,14 @@
+import pytest
+
+from litestar.testing import TestClient
+
+pytestmark = pytest.mark.xdist_group("sqlalchemy_examples")
+
+
+def test_sqlalchemy_declarative_models() -> None:
+    from docs.examples.contrib.sqlalchemy.sqlalchemy_declarative_models import app
+
+    with TestClient(app) as client:
+        response = client.get("/authors")
+        assert response.status_code == 200
+        assert len(response.json()) > 0