Validation Mechanism
The Validation System provides a decorator-based framework for ensuring data integrity across all value objects in the library. It defines a consistent approach for validating input values, enforcing type safety, and raising meaningful error messages when validation fails. This system is the foundation that allows value objects to maintain their invariants and guarantees domain correctness.
The validation system is built on three core components:
@validateDecorator: a decorator that registers validation functions to be executed during value object initialization- Error Hierarchy: a structured set of exception classes for different validation failure scenarios
- Custom Validations: specific validation logic implemented in each value object class
All validation occurs during value object construction, ensuring that invalid objects cannot exist in the system.
Extensibility
The framework is designed to be extensible, allowing developers to add custom validation rules while maintaining consistency across the codebase.
The @validate Decorator
This decorator is the central mechanism for defining validation functions. It can be applied to any method within a value object class.
Validation Method Signature
The decorator is applied to instance methods that accept a value parameter. Each decorated method:
- Receives the value being validated as a parameter
- Performs a specific validation check
- Raises an appropriate exception if validation fails
- Returns None if validation passes
from sindripy.value_objects import ValueObject, validate
class YourValueObject(ValueObject[int]):
@validate
def _ensure_value_is_positive(self, value: int) -> None:
# Your validation logic here
...
Handling Validation Order
When the value object is instantiated, all methods decorated with @validate are executed. The order of execution
can be controlled in two ways:
-
Definition Order: By default, validation methods are executed in the order they are defined in the class.
from sintri.value_objects import ValueObject, validate, SintriValidationError class Integer(ValueObject[int]): @validate def _ensure_has_value(self, value: int) -> None: if value is None: raise SintriValidationError("Value is required") @validate def _ensure_value_is_integer(self, value: int) -> None: if not isinstance(value, int): raise SintriValidationError("Invalid type, expected int") -
Explicit Order: You can specify an
orderparameter in the@validatedecorator to control the sequence explicitly.from sintri.value_objects import ValueObject, validate, SintriValidationError class Integer(ValueObject[int]): @validate(order=1) def _ensure_value_is_integer(self, value: int) -> None: if not isinstance(value, int): raise SintriValidationError("Invalid type, expected int") @validate(order=0) def _ensure_has_value(self, value: int) -> None: if value is None: raise SintriValidationError("Value is required")
Error Hierarchy
The validation system defines a hierarchy of exception classes to represent different scenarios. All
validation errors inherit from the base ValueObjectValidationError class to be able to catch all
validation-related exceptions in a single except block if needed or in an error handler in a FastAPI applications.
Built-in Validation Errors
All built-in value objects raise specific exceptions to provide more context about the type of failure:
RequiredValueError: Raised when a required value is missing (e.g.,None).InvalidTypeError: Raised when the value does not match the expected type.InvalidIdFormatError: Raised when an identifier value does not conform to the expected format.
These errors cannot be used directly in custom value objects, but you can create your own exceptions
and build your own hierarchy that inherits from ValueObjectValidationError.
Custom validations
When creating custom value object, consider creating your own validation
methods and exceptions that inherit from ValueObjectValidationError to provide more specific error
handling in your domain.
You can still use directly ValueObjectValidationError for general validation errors, but custom exceptions can
enhance clarity and maintainability.
from sindripy.value_objects import Float, validate, SindriValidationError
class NegativePriceError(SindriValidationError):
def __init__(self, value: float) -> None:
super().__init__(f"Price cannot have negative value, received '{value}'.")
class Price(Float):
@validate
def _ensure_value_is_positive(self, value: float) -> None:
if value < 0:
raise NegativePriceError(value)
Best Practices
- Use specific exception classes for different validation failures to improve error handling.
- Provide clear and informative error messages to aid debugging and user feedback.
- Keep validation methods focused on a single responsibility for clarity and maintainability.