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: Pydantic V1 DTO Validation for fields with the max_length validator. #3775

Open
2 of 4 tasks
sergeyk93 opened this issue Oct 2, 2024 · 0 comments
Open
2 of 4 tasks
Labels
Bug 🐛 This is something that is not working as expected

Comments

@sergeyk93
Copy link

sergeyk93 commented Oct 2, 2024

Description

In the below example that uses the litestar DTO system with msgspec and pydantic v1, the API validator fails with the following error: litestar.exceptions.http_exceptions.ClientException: 400: Unsupported type: <class 'str'> - at $.display_name.

This is a potential regression after fixing this: #3710 as this case works fine in Litestar 2.11.

Python version: 3.11.9
Pydantic version: 1.10.18

URL to code causing the issue

No response

MCVE

app.py

import time

from litestar import Litestar, Router

from controllers.graph_crud_v1_controller import GraphCrudController


def create_app() -> Litestar:
    # Routes Declarations
    base_v1_router = Router(
        path="/api/v1/accounts/{account_id:str}",
        route_handlers=[
            GraphCrudController,
        ],
    )

    return Litestar(
        route_handlers=[base_v1_router],
        debug=True,
    )


app = create_app()

graph_curd_v1_controller.py

__all__ = ["GraphCrudController"]


import logging

import shortuuid
from litestar import Controller, post
from litestar.dto import DTOData

from dto.test_dtos import GraphReadDto, GraphWriteDto, GraphDto
log = logging.getLogger(__name__)


class GraphCrudController(Controller):
    path = "/graphs"
    dto = GraphWriteDto
    return_dto = GraphReadDto
    temp_data: GraphDto | None = None

    @post(
        path="/create",
        summary="Create Graph",
    )
    async def create_graph(self, account_id: str, data: DTOData[GraphDto]) -> GraphDto:
        log.info(
            "Got a request to create a new graph object in account: %s", account_id
        )
        current_ts = time.time()
        self.temp_data = data.create_instance(
            id=shortuuid.uuid(),
            account_id=account_id,
            created_at=current_ts,
            created_by="mock_user",
            updated_at=current_ts,
            updated_by="mock_user",
        )
        return self.temp_data

test_dtos.py

from typing import Annotated, Any

import shortuuid
from litestar.contrib.pydantic import PydanticDTO
from litestar.dto import DTOConfig
from pydantic import BaseModel
from pydantic.class_validators import validator
from pydantic.config import Extra
from pydantic.fields import Field
import time


class GeneralIdentifiers(BaseModel):
    id: str = Field(default_factory=lambda: shortuuid.uuid())
    created_at: int = Field(default_factory=lambda: time.time())
    created_by: str
    updated_at: int = Field(default_factory=lambda: time.time())
    updated_by: str


class GraphBaseMeta(BaseModel):
    display_name: str = Field(default_factory=lambda: shortuuid.uuid(), max_length=64)
    version: float = Field(default=1.0)
    account_id: str | None = Field(default=None)
    description: str | None = Field(default=None, max_length=600)


class NodePosition(BaseModel):
    x: float
    y: float


class NodeParamData(BaseModel):
    value: Any
    show: bool = True

    class Config:
        extra = Extra.forbid


class Node(BaseModel):
    id: str = Field(default_factory=lambda: shortuuid.uuid())
    type: str
    data: dict[str, NodeParamData]
    position: NodePosition


class Edge(BaseModel):
    id: str = Field(default_factory=lambda: shortuuid.uuid())
    source: str
    target: str


class GraphNodesEdges(BaseModel):
    nodes: list[Node]
    edges: list[Edge]


class GraphBase(GraphBaseMeta, GraphNodesEdges):
    pass


class Graph(GraphBase, GeneralIdentifiers):
    pass


class GraphDto(Graph):
    @validator("nodes")
    def validate_nodes(cls, value: list[Node]) -> list[Node]:
        node_ids: set[str] = set()
        for node in value:
            if node.id:
                if node.id in node_ids:
                    raise ValueError("Duplicate node ids are not allowed")
                node_ids.add(node.id)
        return value


write_config = DTOConfig(
    exclude={
        "id",
        "account_id",
        "created_at",
        "created_by",
        "updated_at",
        "updated_by",
    },
    max_nested_depth=3,
)

read_config = DTOConfig(max_nested_depth=3)

GraphWriteDto = PydanticDTO[Annotated[GraphDto, write_config]]
GraphReadDto = PydanticDTO[Annotated[GraphDto, read_config]]



### Steps to reproduce

```bash
1. Run the application with the above MCVE. The dto class is under the Python package "dto", and the controller class is under the Python package "controllers". app.py located is in the root level of the project.
2. Run the following cURL(change the host/port if yours are configured differently):

curl --location 'localhost:8080/api/v1/accounts/123/graphs/create' \
--header 'Content-Type: application/json' \
--data '{
    "display_name": "Test Graph",
    "edges": [
        {
            "source": "source_test_id",
            "sourceHandle": "handle_test",
            "target": "target_test_id",
            "targetHandle": "handle_test"
        }
    ],
    "public": true,
    "nodes": [
        {
            "id": "source_test_id",
            "base_type": "test",
            "type": "test",
            "position": {
                "x": 10.5,
                "y": 18.31231231
            },
            "data": {
                "name": {
                    "show": true,
                    "value": "test"
                }
            }
        },
        {
            "id": "target_test_id",
            "base_type": "test",
            "type": "test",
            "position": {
                "x": 15.5,
                "y": 32.31231231
            },
            "data": {
                "name": {
                    "show": true,
                    "value": "test"
                }
            }
        }
    ]
}'


### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"

Logs

import sys; print('Python %s on %s' % (sys.version, sys.platform))
/Users/sergeyk/PycharmProjects/litestar-playground/.venv/bin/python -X pycache_prefix=/Users/sergeyk/Library/Caches/JetBrains/PyCharm2024.2/cpython-cache /Applications/PyCharm.app/Contents/plugins/python-ce/helpers/pydev/pydevd.py --module --multiprocess --qt-support=auto --client 127.0.0.1 --port 57713 --file litestar run --host 0.0.0.0 --port 8080 
Connected to pydev debugger (build 242.23339.19)
INFO:     Started server process [99855]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
INFO:     127.0.0.1:57717 - "POST /api/v1/accounts/123/graphs/create HTTP/1.1" 400 Bad Request
ERROR - 2024-10-01 19:48:33,156 - litestar - config - Uncaught exception (connection_type=http, path=/api/v1/accounts/123/graphs/create):
Traceback (most recent call last):
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 139, in default_deserializer
    raise TypeError(f"Unsupported type: {type(value)!r}")
TypeError: Unsupported type: <class 'str'>
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 209, in decode_json
    return msgspec.json.decode(
           ^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Unsupported type: <class 'str'> - at `$.display_name`
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 173, in _get_response_data
    kwargs = await parameter_model.to_kwargs(connection=request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/_kwargs/kwargs_model.py", line 380, in to_kwargs
    await extractor(output, connection)
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/_kwargs/extractors.py", line 484, in extractor
    values["data"] = await data_extractor(connection)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/_kwargs/extractors.py", line 502, in dto_extractor
    return data_dto(connection).decode_bytes(body)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/contrib/pydantic/pydantic_dto_factory.py", line 104, in decode_bytes
    return super().decode_bytes(value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/dto/base_dto.py", line 115, in decode_bytes
    return backend.populate_data_from_raw(value, self.asgi_connection)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/dto/_codegen_backend.py", line 143, in populate_data_from_raw
    data_as_builtins=self._transfer_to_dict(self.parse_raw(raw, asgi_connection)),
                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/dto/_backend.py", line 241, in parse_raw
    result = decode_json(value=raw, target_type=self.annotation, type_decoders=type_decoders, strict=False)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 219, in decode_json
    raise SerializationException(str(msgspec_error)) from msgspec_error
litestar.exceptions.base_exceptions.SerializationException: Unsupported type: <class 'str'> - at `$.display_name`
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/middleware/_internal/exceptions/middleware.py", line 159, in __call__
    await self.app(scope, receive, capture_response_started)
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/_asgi/asgi_router.py", line 100, in __call__
    await asgi_app(scope, receive, send)
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 80, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 132, in _get_response_for_request
    return await self._call_handler_function(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 152, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sergeyk/PycharmProjects/litestar-playground/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 175, in _get_response_data
    raise ClientException(str(e)) from e
litestar.exceptions.http_exceptions.ClientException: 400: Unsupported type: <class 'str'> - at `$.display_name`

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
@sergeyk93 sergeyk93 added the Bug 🐛 This is something that is not working as expected label Oct 2, 2024
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
None yet
Development

No branches or pull requests

1 participant