litestar-org/litestar

Bug: `return_dto` with optional nested `Struct` field raises 500 error when no default exists

bdoms opened this issue · 0 comments

Description

I read through every open bug and couldn't find something directly comparable (though in general it seems like nested objects in DTOs is a recurring source of issues).

The crux is that I don't want to provide a default for the None case of a field because I want to explicitly set it, even if it's None. That works fine if the field is a simple type like str but when it's a nested type like a Struct of its own then an error appears stating TypeError: Missing required argument even when the argument is provided.

URL to code causing the issue

No response

MCVE

from litestar import Litestar, get
from litestar.dto import MsgspecDTO
from msgspec import Struct


class Nested(Struct):
    item: str


class IWorkOut(Struct):
    foo: Nested | None = None

@get('/works', return_dto=MsgspecDTO[IWorkOut])
async def works() -> IWorkOut:
    return IWorkOut(foo=None)


class IFail(Struct):
    # the only difference here vs `IWorkOut` above is not including `= None`
    # also note that changing `Nested` to a simple type like `str` here makes the error go away
    foo: Nested | None

@get('/fails', return_dto=MsgspecDTO[IFail])
async def fails() -> IFail:
    return IFail(foo=None)


app = Litestar([works, fails])


Worth noting: I'm reporting this here and not with Msgspec because just running `IFail(foo=None)` by itself works correctly. The issue appears to only occur because of the `return_dto`.

Steps to reproduce

1. Run the app, I use uvicorn: `uvicorn main:app`
2. `GET /works` and it correctly returns a 200 with `{"foo": null}`
3. `GET /fails` and it returns a 500 with `{"status_code":500,"detail":"Internal Server Error"}`
4. See error logging below

Screenshots

No response

Logs

INFO:     127.0.0.1:49410 - "GET /fails HTTP/1.1" 500 Internal Server Error
ERROR - 2024-10-12 21:39:00,401 - root - example - Missing required argument 'foo'
Traceback (most recent call last):
  File "[...]/litestar/middleware/_internal/exceptions/middleware.py", line 159, in __call__
    await self.app(scope, receive, capture_response_started)
  File "[...]/litestar/_asgi/asgi_router.py", line 100, in __call__
    await asgi_app(scope, receive, send)
  File "[...]/litestar/routes/http.py", line 80, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "[...]/litestar/routes/http.py", line 132, in _get_response_for_request
    return await self._call_handler_function(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "[...]/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/handlers/http_handlers/base.py", line 555, in to_response
    data = return_dto_type(request).data_to_encodable_type(data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "[...]/litestar/dto/base_dto.py", line 119, in data_to_encodable_type
    return backend.encode_data(data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "[...]/litestar/dto/_codegen_backend.py", line 161, in encode_data
    return cast("LitestarEncodableType", self._encode_data(data))
                                         ^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 33, in func
TypeError: Missing required argument 'foo'

Litestar Version

2.12.1

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