#5051·litestar

msgspec.Meta constraints on collection/mapping members are silently dropped in DTO transfer models (same root cause as #5031)

Author: blockgrootCreated Sep 7, 2026Updated Sep 7, 2026

Description

#5031 reported that a msgspec.Meta constraint on one arm of a heterogeneous union (e.g. Union[Annotated[str, Meta(min_length=5)], int]) is silently dropped when the DTO transfer model is built, so MsgspecDTO accepts data that plain msgspec.json.decode would reject. PR #5045 fixes that for union arms specifically.

The same root cause also affects collection items and mapping values, which #5045 does not touch: _create_transfer_model_type_annotation's SimpleType branch (litestar/dto/_backend.py) always returns the member's bare .annotation, discarding any Meta that lives in .metadata, for every caller that recurses into a member type - not just _create_transfer_model_union_type.

To Reproduce

python
from typing import Annotated, List
from litestar import Litestar, post
from litestar.dto import MsgspecDTO
from msgspec import Struct, Meta

class ListStruct(Struct):
    items: List[Annotated[str, Meta(min_length=5)]]

@post("/list", dto=MsgspecDTO[ListStruct])
async def listy(data: ListStruct) -> ListStruct:
    return data

app = Litestar(route_handlers=[listy])

curl -d '{"items": ["12"]}' http://localhost:8000/list returns 201 with {"items":["12"]} instead of a 400 - "12" is 2 characters, violating min_length=5. The same holds for Dict[str, Annotated[str, Meta(min_length=5)]] values.

Expected behavior

A Meta constraint on a collection item or mapping value should be enforced the same way it already is for a plain field or (after #5045) a union arm.

Root cause

litestar/dto/_backend.py, _create_transfer_model_type_annotation's SimpleType branch returns transfer_type.field_definition.annotation unconditionally. This function is called recursively by _create_transfer_model_collection_type, _create_transfer_model_tuple_type, _create_transfer_model_mapping_type and _create_transfer_model_union_type alike, so fixing only the union path (as #5045 does) leaves the identical gap in the other three.

Litestar Version

main @ cbc0c51 (also present as of 2.24.0)