Constraints¶
When using Typed Decoding structtype will ensure decoded
messages match the specified types. For example, to decode a list of integers
from JSON:
>>> from structtype import Struct
>>> class IntList(Struct):
... items: list[int]
>>> IntList.struct_validate_json(b'{"items": [1, 2, 3]}')
IntList(items=[1, 2, 3])
>>> IntList.struct_validate_json(b'{"items": [1, 2, "oops"]}')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `int`, got `str` - at `$.items[2]`
Often this is sufficient, but sometimes you also need to impose constraints on the values (rather than the types) found in the message.
Constraints in structtype are specified by wrapping a type with
typing.Annotated, and adding a structtype.Field annotation.
For example, to constrain the list to positive integers (> 0), you’d make
use of the gt (greater-than) constraint:
>>> from typing import Annotated
>>> from structtype import Field, Struct
>>> PositiveInt = Annotated[int, Field(gt=0)]
>>> class PosList(Struct):
... items: list[PositiveInt]
>>> PosList.struct_validate_json(b'{"items": [1, 2, 3]}')
PosList(items=[1, 2, 3])
>>> PosList.struct_validate_json(b'{"items": [1, 2, -1]}')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `int` >= 1 - at `$.items[2]`
Constraints can be combined to enforce complex requirements. Here’s a more
complete example enforcing the following constraints on a User struct:
nameis astrwith1 <= length <= 32matching the regular expression"^[a-z_][a-z0-9_-]*$".groupsis asetof at most 16 strings, each with the same constraints asnameabove, defaulting to the emptyset.cpu_limitis afloatwith a value>= 0.1and<= 8, defaulting to 1.mem_limitis anintwith a value>= 256and<= 8192, defaulting to 1024.
>>> from typing import Annotated
>>> from structtype import Struct, Field
>>> UnixName = Annotated[
... str, Field(min_length=1, max_length=32, pattern="^[a-z_][a-z0-9_-]*$")
... ]
>>> class User(Struct):
... name: UnixName
... groups: Annotated[set[UnixName], Field(max_length=16)] = set()
... cpu_limit: Annotated[float, Field(ge=0.1, le=8)] = 1
... mem_limit: Annotated[int, Field(ge=256, le=8192)] = 1024
As shown above, Annotated types can applied inline, or used to create type
aliases and then reused elsewhere (as done with UnixName).
The following constraints are supported:
Numeric Constraints¶
These constraints are valid on int or float types:
ge: The value must be greater than or equal toge.gt: The value must be greater thangt.le: The value must be less than or equal tole.lt: The value must be less thanlt.multiple_of: The value must be a multiple ofmultiple_of.
>>> from typing import Annotated
>>> from structtype import Struct, Field
>>> class Value(Struct):
... val: Annotated[int, Field(ge=0)]
>>> Value.struct_validate_json(b'{"val": -1}')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `int` >= 0 - at `$.val`
Warning
While multiple_of works on float types, we don’t recommend
specifying non-integral multiple_of constraints, as they may be
erroneously marked as invalid due to floating point precision issues. For
example, annotating a float type with multiple_of=10 is fine, but
multiple_of=0.1 may lead to issues. See this GitHub issue for more
details.
String Constraints¶
These constraints are valid on str types:
min_length: The minimum valid length, inclusive.max_length: The maximum valid length, inclusive.pattern: A regular expression pattern that the value must match. Note that patterns are treated as unanchored. This means that the pattern “es” matches not just “es” but also “expression”. If required, you must explicitly anchor the pattern by adding a “^” prefix and “$” suffix. For example, the pattern “^es$” only matches the string “es”
>>> from typing import Annotated
>>> from structtype import Struct, Field
>>> class UserName(Struct):
... name: Annotated[str, Field(pattern="^[a-z0-9_]*$")]
>>> UserName.struct_validate_json(
... b'{"name": "invalid username"}',
... )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `str` matching regex '^[a-z0-9_]*$' - at `$.name`
Datetime Constraints¶
These constraints are valid on datetime.datetime and datetime.time types:
tz: Whether the annotated type is required to be timezone-aware. Set toTrueto require timezone-aware values, orFalseto require timezone-naive values. The default isNone, which accepts either timezone-aware or timezone-naive values.
>>> from typing import Annotated
>>> from structtype import Struct, Field
>>> from datetime import datetime
>>> class EventTZ(Struct):
... at: Annotated[datetime, Field(tz=True)]
>>> EventTZ.struct_validate_json(
... b'{"at": "2022-04-02T18:18:10"}',
... ) # require timezone aware
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `datetime` with a timezone component - at `$.at`
>>> class EventNaive(Struct):
... at: Annotated[datetime, Field(tz=False)]
>>> EventNaive.struct_validate_json(
... b'{"at": "2022-04-02T18:18:10-06:00"}',
... ) # require timezone naive
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `datetime` with no timezone component - at `$.at`
Bytes Constraints¶
These constraints are valid on bytes and bytearray types:
min_length: The minimum valid length, inclusive.max_length: The maximum valid length, inclusive.
>>> from typing import Annotated
>>> from structtype import Struct, Field
>>> class Payload(Struct):
... data: Annotated[bytes, Field(min_length=10)]
>>> Payload.struct_validate_json(
... b'{"data": "ZXhhbXBsZQ=="}',
... )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `bytes` of length >= 10 - at `$.data`
Sequence Constraints¶
These constraints are valid on list, tuple, set, and frozenset types:
min_length: The minimum valid length, inclusive.max_length: The maximum valid length, inclusive.
>>> from typing import Annotated
>>> from structtype import Struct, Field
>>> class SmallList(Struct):
... items: Annotated[list[int], Field(max_length=3)]
>>> SmallList.struct_validate_json(
... b'{"items": [1, 2, 3, 4]}',
... )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `array` of length <= 3 - at `$.items`
Mapping Constraints¶
These constraints are valid on dict types:
min_length: The minimum valid length, inclusive.max_length: The maximum valid length, inclusive.
>>> from typing import Annotated
>>> from structtype import Struct, Field
>>> class SmallDict(Struct):
... items: Annotated[dict[str, int], Field(max_length=3)]
>>> SmallDict.struct_validate_json(
... b'{"items": {"a": 1, "b": 2, "c": 3, "d": 4}}',
... )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
structtype.ValidationError: Expected `object` of length <= 3 - at `$.items`