Skip to content

Support constraints on Optional (union with None) types - #994

Closed
Siyet wants to merge 1 commit into
msgspec:mainfrom
Siyet:804-optional-constraints
Closed

Siyet wants to merge 1 commit into
msgspec:mainfrom
Siyet:804-optional-constraints

Conversation

@Siyet

@Siyet Siyet commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Derive constraint_kind from non-None union members so that Annotated[X | None, Meta(...)] constraints are validated correctly
  • Previously any constraint on an Optional[X] type raised TypeError because the union branch left kind as CK_OTHER
  • Supports all constraint kinds: numeric (gt/ge/lt/le/multiple_of), string (min_length/max_length/pattern), bytes, collections, datetime/time (tz)

Closes #804

Test plan

All 275 constraint tests pass (Ubuntu 22.04, Python 3.10):

tests/unit/test_constraints.py - 275 passed in 0.27s

New tests added:

  • test_optional_str_min_length - Annotated[Union[str, None], Meta(min_length=3)]
  • test_optional_int_gt - Annotated[Union[int, None], Meta(gt=0)]
  • test_optional_float_ge - Annotated[Union[float, None], Meta(ge=0.0)]

Each test verifies:

  • None values pass through without constraint checks

  • Valid values are accepted

  • Invalid values raise ValidationError

  • CI passes on all platforms

Derive constraint_kind from non-None union members so that
Annotated[X | None, Meta(...)] constraints are validated correctly.
Previously any constraint on an Optional[X] type raised TypeError
because the union branch left kind as CK_OTHER.

Closes #804
@Siyet

Siyet commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

CI failures are unrelated to this change:

"Profile Windows (ARM64)" - runner infra issue ("Install command runner" step)
"build" (documentation) - link checker fails on a pre-existing broken link (#976)
All build, test, and wheel jobs pass across all platforms.

@Siyet

Siyet commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

Resolved in the community fork msgspec-arise: PR #10, released in v0.20.2.

@jcrist

jcrist commented Apr 8, 2026

Copy link
Copy Markdown
Member

Not supporting this was intentional.

We only support applying annotations directly to the type they apply to for clarity.

# This works, and makes it clear that the annotation applies to the `int`
Annotated[int, Meta(ge=1)] | None

# This doesn't work, but IMO it shouldn't. None isn't numeric and can't support >=
Annotated[int | None, Meta(ge=1)]

Since Optional[int] is the same as int | None, it's a slippery slope to applying annotations to a union and expecting them to apply to all union members. Annotated[int | str | None, Meta(ge=1)] for example. I'd rather just keep things simple and clear - annotations must be applied to the type directly, and not the union.

@jcrist jcrist closed this Apr 8, 2026
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.

Cannot set min_length for optional string filed

2 participants