Skip to content

Bug: PydanticDTO fails validation for list of nested Pydantic models (list[Model] | None) #4530

@sergeyk-fw

Description

@sergeyk-fw

Description

PydanticDTO fails to validate request payloads containing list[NestedModel] | None fields, while native Pydantic handles them correctly.

Python version: 3.12

MCVE reproduces the issue.
Expected Behaviour of the MCVE:
Both endpoints should return 201 Created since the payload is valid.

Actual Behaviour of the MCVE:
The /dto endpoint returns 400 Bad Request while the native pydantic(/native) endpoint returns 201 as expected:
{ "status_code": 400, "detail": "Bad Request", "extra": [ { "type": "model_type", "loc": ["tool_calls", 0], "msg": "Input should be a valid dictionary or instance of ToolCall", "input": {"id": "call_1", "type": "function", "function": {"name": "fn", "arguments": "{}"}}, "ctx": {"class_name": "ToolCall"} } ] }

Additional Notes:

  • Simple optional nested models (UsageMetadata | None) work correctly with PydanticDTO
  • The issue is specific to list[NestedModel] | None fields

URL to code causing the issue

No response

MCVE

from litestar import Litestar, post
from litestar.dto import DTOConfig
from litestar.plugins.pydantic import PydanticDTO
from litestar.testing import TestClient
from pydantic import BaseModel


class ToolCallFunction(BaseModel):
    name: str
    arguments: str


class ToolCall(BaseModel):
    id: str
    type: str = "function"
    function: ToolCallFunction


class CreateMessageRequest(BaseModel):
    session_id: str
    content: str
    tool_calls: list[ToolCall] | None = None


# Without DTO - works
@post("/native")
async def native_handler(data: CreateMessageRequest) -> dict:
    return {"tool_count": len(data.tool_calls) if data.tool_calls else 0}


# With DTO - fails
class CreateMessageDTO(PydanticDTO[CreateMessageRequest]):
    config = DTOConfig(max_nested_depth=10)


@post("/dto", dto=CreateMessageDTO)
async def dto_handler(data: CreateMessageRequest) -> dict:
    return {"tool_count": len(data.tool_calls) if data.tool_calls else 0}


app = Litestar([native_handler, dto_handler])

payload = {
    "session_id": "s1",
    "content": "test",
    "tool_calls": [
        {"id": "call_1", "type": "function", "function": {"name": "fn", "arguments": "{}"}}
    ],
}

with TestClient(app) as client:
    r1 = client.post("/native", json=payload)
    print(f"Native Pydantic: {r1.status_code}")  # 201

    r2 = client.post("/dto", json=payload)
    print(f"With DTO:        {r2.status_code}")  # 400
    print(r2.json())

Steps to reproduce

Run MCVE

Screenshots

No response

Logs


Litestar Version

2.19

Platform

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Bug 🐛This is something that is not working as expected

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions