diff --git a/docs/release-notes/changelog.rst b/docs/release-notes/changelog.rst index fa23e76c69..02fc9335c2 100644 --- a/docs/release-notes/changelog.rst +++ b/docs/release-notes/changelog.rst @@ -362,3 +362,9 @@ Add a new attribute :attr:`~litestar.middleware.ASGIMiddleware.should_bypass_for_scope`; A callable which takes in a :class:`~litestar.types.Scope` and returns a boolean to indicate whether to bypass the middleware for the current request. + + .. change:: Fix KeyError when ClassVar exists on msgspec Struct + :type: bugfix + :pr: 4665 + + Fix a bug in :class:`MsgspecDTO` where a KeyError was raised if a :class:`msgspec.Struct` contained a :class:`~typing.ClassVar`. ClassVars are now correctly skipped when generating field definitions. diff --git a/litestar/dto/msgspec_dto.py b/litestar/dto/msgspec_dto.py index bb44342d0c..7dc6d3840a 100644 --- a/litestar/dto/msgspec_dto.py +++ b/litestar/dto/msgspec_dto.py @@ -48,6 +48,8 @@ def generate_field_definitions(cls, model_type: type[Struct]) -> Generator[DTOFi property_fields = cls.get_property_fields(model_type) for key, field_definition in cls.get_model_type_hints(model_type).items(): + if key not in inspect_fields: + continue kwarg_definition, extra = kwarg_definition_from_field(inspect_fields[key]) field_definition = dataclasses.replace(field_definition, kwarg_definition=kwarg_definition) field_definition.extra.update(extra) diff --git a/tests/unit/test_contrib/test_msgspec.py b/tests/unit/test_contrib/test_msgspec.py index 4aeca7dc0e..aec2d97d8a 100644 --- a/tests/unit/test_contrib/test_msgspec.py +++ b/tests/unit/test_contrib/test_msgspec.py @@ -2,7 +2,7 @@ import itertools from dataclasses import replace -from typing import TYPE_CHECKING, Annotated +from typing import TYPE_CHECKING, Annotated, ClassVar from unittest.mock import ANY import pytest @@ -216,3 +216,15 @@ def handler_3(data: Model3) -> None: "required": ["foo", "regular_field"], "title": "Model3", } + + +def test_msgspec_dto_with_classvar() -> None: + class ModelWithClassVar(Struct): + regular_field: str + class_field: ClassVar[str] = "a string in the class" + + field_defs = list(MsgspecDTO.generate_field_definitions(ModelWithClassVar)) + + # Only the regular field should be included, not the ClassVar + assert len(field_defs) == 1 + assert field_defs[0].name == "regular_field"