Skip to content

fix(dto): apply msgspec constraints to the constrained union member - #4988

Open
NoiceHax wants to merge 3 commits into
litestar-org:mainfrom
NoiceHax:fix/issue-4181
Open

fix(dto): apply msgspec constraints to the constrained union member#4988
NoiceHax wants to merge 3 commits into
litestar-org:mainfrom
NoiceHax:fix/issue-4181

Conversation

@NoiceHax

@NoiceHax NoiceHax commented Aug 14, 2026

Copy link
Copy Markdown

Transfer models wrap optional and partial fields in a union with None or UnsetType, but constraints copied from the source model were attached to the union itself, giving annotations like Annotated[Union[str, UnsetType], Meta(min_length=8)]. msgspec only accepts type specific constraints on the type they apply to and raises a TypeError for that, so any DTO with a constrained optional field or partial=True failed to build.

The meta now goes on the single constrainable member of the union. If a union has more than one member that could take constraints there is nowhere obvious to put it, so it stays on the union as before. Partial DTOs no longer need to drop min_length and max_length, which was a workaround for the same problem.

The old workaround test is replaced by a parametrized one covering each constraint on both backends, and I added tests for Union[Annotated[str, Meta], UnsetType] and Optional[Annotated[str, Meta]]. They are in tests/unit/test_dto/test_factory/test_integration.py and fail without the change.

Closes #4181


📚 Documentation preview 📚: https://litestar-org.github.io/litestar-docs-preview/4988

Transfer models wrap the type of optional and partial fields in a union with
`None`/`UnsetType`, but the constraints extracted from the source model were
attached to the union itself, producing annotations such as
`Annotated[Union[str, UnsetType], Meta(min_length=8)]`. msgspec only accepts
type specific constraints on the type they apply to and raises a `TypeError`
for those, so any DTO with a constrained optional field or a `partial` config
failed to build. The metadata is now applied to the single constrainable union
member instead, which also makes dropping length constraints for partial DTOs
- a workaround for the same limitation - unnecessary.

Closes litestar-org#4181
@NoiceHax
NoiceHax requested review from a team as code owners August 14, 2026 05:08
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.42%. Comparing base (7e56d30) to head (a1e2748).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
litestar/dto/_backend.py 70.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4988      +/-   ##
==========================================
+ Coverage   67.41%   67.42%   +0.01%     
==========================================
  Files         293      293              
  Lines       15308    15316       +8     
  Branches     1736     1738       +2     
==========================================
+ Hits        10320    10327       +7     
- Misses       4839     4841       +2     
+ Partials      149      148       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

`_annotate_with_meta` only moves the metadata onto a union member when there is
exactly one member that can carry it. The fallback path, where the union has
more than one such member and the metadata stays on the union itself, had no
test. Add one so the branch is exercised.
Comment thread litestar/dto/_backend.py Outdated
if field_definition.passthrough_constraints:
if (field_meta := _create_struct_field_meta_for_field_definition(field_definition)) is not None:
field_type = Annotated[field_type, field_meta]
field_type = _annotate_with_meta(field_type, field_meta)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be easier to check field_definition.is_partial than do the type unwrapping? iiuc this case only happens for partial fields where we add an outer Union. In other cases, we do not alter the type, so a wrongly attached Meta is a user error, which we should not silently fix.

Remove the _annotate_with_meta helper and its extra imports (get_args,
is_union, NoneType). Apply the constraint annotation to the inner type
before wrapping in Union only when field_definition.is_partial is set,
which is the only case where we add the outer Union ourselves. For all
other cases the user's own annotation is untouched (a wrongly attached
Meta is a user error).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: CodegenBackend rewrites field type incorrectly, causing msgspec error

2 participants