#5031·litestar

Bug: msgspec.Meta getting lost silently if field in Struct typed as union type of 2 and more types where one with Meta

Author: happykustCreated Sep 1, 2026Updated Sep 1, 2026
LabelsBug :bug:

Description

While I've researching Issue #5025 and experimenting with types, I got unexpected (by logic) behavior.

If we write unioned type for field in request's struct where one type uses msgspec.Meta and the second any type, excluded None, we silently getting lost msgspec.Meta object from type.

Litestar allowing to me send string with any length in text field, while msgspec will raise ValidationError:

>>> import msgspec
>>> from typing import Union, Annotated
>>> T = Union[Annotated[str, msgspec.Meta(description="only-str", min_length=5)], int]
>>> msgspec.json.decode(b'1', type=T)
1
>>> msgspec.json.decode(b'"12345"', type=T)
'12345'
>>> msgspec.json.decode(b'"12"', type=T)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
msgspec.ValidationError: Expected `str` of length >= 5

I found the merged PR #4815 and _unwrap_optional helper. There is the test_msgspec_union_with_metadata_arm_is_not_unwrapped test case that tests the OpenAPI schema. Test creates the same type and checks that we no more have description and conditions for our type that we declared in msgspec.Meta class earlier. But this test case also applies to DTO (this issue).

Error in OpenAPI schema may be less serious, but DTO silently skips validation and accepts invalid incoming data.

So what is that: mistake or an expected behavior? I'm ready to try fixing it =)

URL to code causing the issue

No response

MCVE

python
from typing import Annotated, Union

from litestar import Litestar, post
from litestar.dto import MsgspecDTO
from msgspec import Struct, Meta

class TestStruct(Struct):
    text: Union[Annotated[str, Meta(description="only-str", min_length=5)], int]


@post('/hello', dto=MsgspecDTO[TestStruct])
async def hello(data: TestStruct) -> T:
    print(f"incoming text: {data.text}")
    return data.text


app = Litestar(route_handlers=[hello])

Steps to reproduce

  1. Run the MCVE with uvicorn
  2. Send request with 1 as integer value in text field curl --header "Content-Type: application/json" --request POST --data '{"text": 1}' http://localhost:8000/hello
  3. See "incoming text: 1" in app logs and 201 HTTP code. Thats expected behavior: we allow send any integer to text field
  4. Send request with string value that shorter than 5 curl --header "Content-Type: application/json" --request POST --data '{"text": "123"}' http://localhost:8000/hello
  5. See "incoming text: 123" in app logs and 201 HTTP code again. Thats not expected behavior: we do not allow to send string in text field shorter than 5

Screenshots

No response

Logs

Litestar Version

The main branch. But I think it also appears at 2.24.0

Platform

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