Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug(SQLAlchemy Plugin): Generic responses are not getting serialized #3553

Open
1 of 4 tasks
Alc-Alc opened this issue Jun 7, 2024 · 2 comments
Open
1 of 4 tasks
Labels
Bug 🐛 This is something that is not working as expected

Comments

@Alc-Alc
Copy link
Contributor

Alc-Alc commented Jun 7, 2024

Description

If the return type of a route has more than one field which is generic on one or two (possibly more?) types the expected serialization does not occur.

TL;DR
Current behavior: Serialization Fails for all routes except route-0
Expected behavior: Serialization Success

URL to code causing the issue

No response

MCVE

from dataclasses import dataclass
from typing import Generic, TypeVar
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from litestar import Response, get
from litestar.contrib.sqlalchemy.plugins import SQLAlchemySerializationPlugin
from litestar.testing import create_test_client

class Base(DeclarativeBase):
    pass

class SomeTable(Base):
    __tablename__ = "something"
    id: Mapped[int] = mapped_column(primary_key=True)
    col: Mapped[str]

T = TypeVar("T")
U = TypeVar("U")

@dataclass
class GenericOnOne(Generic[T]):
    val_1: list[T]

@dataclass
class GenericOnTwo(Generic[T, U]):
    val_1: list[T]
    val_2: list[U]

@dataclass
class GenericOnOneButTwoFields(Generic[T]):
    val_1: list[T]
    val_2: list[T]

t = [SomeTable(id=1, col="Hello, World!")]

@get("route-0")
async def route_0() -> GenericOnOne[SomeTable]:
    # nothing wrong here, just to show it works
    return GenericOnOne(val_1=t)

@get("route-1")
async def route_1() -> GenericOnTwo[SomeTable, SomeTable]:
    # fails to serialize if it has two fields generic on different types
    return GenericOnTwo(val_1=t, val_2=t)

@get("route-2")
async def route_2() -> GenericOnOneButTwoFields[SomeTable]:
    # fails to serialize if has two fields generic on the same types
    return GenericOnOneButTwoFields(val_1=t, val_2=t)

@get("route-3")
async def route_3() -> Response[GenericOnOne[SomeTable]]:
    # fails to serialize when wrapped with another Generic
    return Response(GenericOnOne(val_1=t))

with create_test_client(
    [route_0, route_1, route_2, route_3], plugins=[SQLAlchemySerializationPlugin()]
) as client:
    response_0 = client.get("route-0")
    assert response_0.status_code == 200

    response_1 = client.get("route-1")
    assert response_1.status_code == 500

    response_2 = client.get("route-2")
    assert response_2.status_code == 500

    response_3 = client.get("route-3")
    assert response_3.status_code == 500

Steps to reproduce

Run the code

Screenshots

"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"

Logs

Traceback (most recent call last):
  File "litestar/litestar/serialization/msgspec_hooks.py", line 162, in encode_json
    return msgspec.json.encode(value, enc_hook=serializer) if serializer else _msgspec_json_encoder.encode(value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "litestar/litestar/serialization/msgspec_hooks.py", line 92, in default_serializer
    raise TypeError(f"Unsupported type: {type(value)!r}")
TypeError: Unsupported type: <class '__main__.SomeTable'>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "litestar/litestar/middleware/_internal/exceptions/middleware.py", line 158, in __call__
    await self.app(scope, receive, capture_response_started)
  File "litestar/litestar/_asgi/asgi_router.py", line 99, in __call__
    await asgi_app(scope, receive, send)
  File "litestar/litestar/routes/http.py", line 80, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "litestar/litestar/routes/http.py", line 132, in _get_response_for_request
    return await self._call_handler_function(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "litestar/litestar/routes/http.py", line 156, in _call_handler_function
    response: ASGIApp = await route_handler.to_response(app=scope["app"], data=response_data, request=request)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "litestar/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 "litestar/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 "litestar/litestar/response/base.py", line 451, in to_asgi_response
    body=self.render(self.content, media_type, get_serializer(type_encoders)),
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "litestar/litestar/response/base.py", line 392, in render
    return encode_json(content, enc_hook)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "litestar/litestar/serialization/msgspec_hooks.py", line 164, in encode_json
    raise SerializationException(str(msgspec_error)) from msgspec_error
litestar.exceptions.base_exceptions.SerializationException: Unsupported type: <class '__main__.SomeTable'>

Litestar Version

84f51c8 (main as of issue creation)

Platform

  • Linux
  • Mac
  • Windows
  • Other (Please specify in the description above)

Note

While we are open for sponsoring on GitHub Sponsors and
OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh dashboard

  • 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.
Fund with Polar
@Alc-Alc Alc-Alc added the Bug 🐛 This is something that is not working as expected label Jun 7, 2024
@EliasEriksson
Copy link

Another MCVE that seams to be causing this issue as discussed on discord (an example from the docs wrapped with litestar.Response):

MVCE

import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import Generic, TypeVar
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy import text
from sqlalchemy import Uuid
from sqlalchemy.orm import DeclarativeBase
from litestar import Litestar, get
from litestar import Response
from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
from litestar.dto import DTOConfig
 
 
T = TypeVar("T")
 
 
class Base(DeclarativeBase):
    pass
 
 
@dataclass
class WithCount(Generic[T]):
    count: int
    data: list[T]
 
 
class User(Base):
    __tablename__ = "user"
    id: Mapped[uuid.UUID] = mapped_column(
        Uuid(as_uuid=True, native_uuid=True),
        primary_key=True,
        nullable=False,
        server_default=text("gen_random_uuid()"),
    )
    name: Mapped[str]
    password: Mapped[str]
    created_at: Mapped[datetime]
 
 
class UserDTO(SQLAlchemyDTO[User]):
    config = DTOConfig(exclude={"password", "created_at"})
 
 
@get("/api/users", dto=UserDTO, sync_to_thread=False)
def get_users() -> Response[WithCount[User]]:
    return Response(
        WithCount(
            count=1,
            data=[
                User(
                    id=uuid.uuid4(),
                    name="Litestar User",
                    password="xyz",
                    created_at=datetime.now(),
                ),
            ],
        )
    )
 
 
api = Litestar(route_handlers=[get_users], debug=True)

crashes when requesting GET /api/users

Logs:

INFO:     127.0.0.1:47442 - "GET /api/users HTTP/1.1" 500 Internal Server Error
ERROR - 2024-06-08 17:55:09,723 - litestar - config - Uncaught exception (connection_type=http, path=/api/users):
Traceback (most recent call last):
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/serialization/msgspec_hooks.py", line 162, in encode_json
    return msgspec.json.encode(value, enc_hook=serializer) if serializer else _msgspec_json_encoder.encode(value)
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/serialization/msgspec_hooks.py", line 92, in default_serializer
    raise TypeError(f"Unsupported type: {type(value)!r}")
TypeError: Unsupported type: <class 'api.api.User'>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/middleware/_internal/exceptions/middleware.py", line 158, in __call__
    await self.app(scope, receive, capture_response_started)
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/_asgi/asgi_router.py", line 99, in __call__
    await asgi_app(scope, receive, send)
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/routes/http.py", line 80, in handle
    response = await self._get_response_for_request(
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/routes/http.py", line 132, in _get_response_for_request
    return await self._call_handler_function(
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/routes/http.py", line 156, in _call_handler_function
    response: ASGIApp = await route_handler.to_response(app=scope["app"], data=response_data, request=request)
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/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 "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/handlers/http_handlers/_utils.py", line 152, in handler
    return response.to_asgi_response(  # type: ignore[no-any-return]
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/response/base.py", line 451, in to_asgi_response
    body=self.render(self.content, media_type, get_serializer(type_encoders)),
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/response/base.py", line 392, in render
    return encode_json(content, enc_hook)
  File "/home/elias-eriksson/dev/eliaseriksson/fullstack-template/venv/lib/python3.10/site-packages/litestar/serialization/msgspec_hooks.py", line 164, in encode_json
    raise SerializationException(str(msgspec_error)) from msgspec_error
litestar.exceptions.base_exceptions.SerializationException: Unsupported type: <class 'api.api.User'>

@Alc-Alc
Copy link
Contributor Author

Alc-Alc commented Jun 8, 2024

Another MCVE that seams to be causing this issue as discussed on discord (an example from the docs wrapped with litestar.Response):

Thanks I have added the use case to the tests above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug 🐛 This is something that is not working as expected
Projects
Status: Triage
Development

No branches or pull requests

2 participants