Validating model values¶
A range’s start must not exceed its end. This rule relates two fields and cannot be expressed by choosing a numeric codec:
from bytespec import ProtoModel
from bytespec.types import UInt16
class Window(ProtoModel):
__header__ = ()
start: UInt16
end: UInt16
def __validate__(self) -> None:
if self.start > self.end:
raise ValueError("start must not exceed end")
window = Window(start=10, end=20)
assert Window.decode(window.encode()) == window
try:
Window(start=20, end=10)
except ValueError as error:
print(error)
start must not exceed end
The library validates the binary schema itself: whether types are known and indices and settings are valid. A schema error raises SchemaError. __validate__ checks application values. It is an ordinary instance method with no decorator; when it runs, all fields have already been assigned, including defaults and factory results. Its return value is ignored: raise an exception to reject the values.
When validation runs¶
The method runs at the end of ProtoModel.__init__, so it also runs during decode() and decode_from(). Decoding the bytes creates an ordinary instance. For example, valid numbers can violate the range rule:
try:
Window.decode(bytes.fromhex("00 14 00 0a"))
except ValueError as error:
print(error)
start must not exceed end
The validator’s exception propagates unchanged: here it is ValueError, not DecodeError or EncodeError. Choose an exception type the application can handle alongside decoding errors.
Attribute assignment and encode() do not run validation again:
window.start = 30
print(window.encode().hex(" "))
try:
window.__validate__()
except ValueError as error:
print(error)
00 1e 00 14
start must not exceed end
If you mutate the object, call the method explicitly before encoding, or create a new instance. Codecs still check numeric ranges and whether values can be represented during encode(). Models do not provide full runtime type checking or automatic value conversion.
Validation in a subclass¶
Automatic invocation looks for __validate__ only in the concrete class itself. An inherited method is available in Python but does not run automatically:
class UncheckedWindow(Window):
pass
unchecked = UncheckedWindow(start=20, end=10)
assert unchecked.start > unchecked.end
To reuse it, define a method in the child class:
class CheckedWindow(Window):
def __validate__(self) -> None:
super().__validate__()
try:
CheckedWindow(start=20, end=10)
except ValueError as error:
print(error)
start must not exceed end
A nested model runs its own validation when it is created, before the outer model’s validator during decoding. Further rules for composition and subclass schemas are described in Model inheritance.
Next: Handling errors explains which exceptions to handle when exchanging messages.