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.