Validation¶
Varlord provides comprehensive built-in validators and supports custom validation.
Validation is performed in the dataclass __post_init__ method, which is
automatically called when the configuration is instantiated.
Important: Validation happens after all sources are merged. This means:
Model defaults are automatically applied first
All configuration sources are loaded and merged
The merged configuration is converted to a model instance
Required field validation is performed (if enabled)
__post_init__is called, which performs value validationIf validation fails, the entire configuration load fails
This ensures that validation works on the final merged values, not just defaults.
Required Field Validation:
Fields are automatically determined as required/optional:
- Fields without defaults and not Optional[T] are required
- Fields with Optional[T] type annotation are optional
- Fields with defaults (or default_factory) are optional
Required fields are validated before __post_init__ is called.
Example:
from dataclasses import dataclass, field
from varlord import Config, sources
from varlord.validators import validate_length
from varlord.model_validation import RequiredFieldError
@dataclass(frozen=True)
class AppConfig:
api_key: str = field() # Required - no default
# This will FAIL - api_key not provided
cfg = Config(model=AppConfig, sources=[])
try:
app = cfg.load() # Raises RequiredFieldError
except RequiredFieldError as e:
print(f"Missing required fields: {e.missing_fields}")
# This will SUCCEED (env provides api_key)
# Set: export API_KEY="a" * 32
cfg = Config(
model=AppConfig,
sources=[
sources.Env(),
],
)
app = cfg.load() # OK - required field provided
Built-in Validators¶
Varlord provides a comprehensive set of validators organized by category:
Basic Validators¶
validate_range: Validate that a value is within a range.
from varlord.validators import validate_range
validate_range(50, min=0, max=100) # OK
validate_range(150, min=0, max=100) # Raises ValidationError
validate_regex: Validate that a string matches a regex pattern.
from varlord.validators import validate_regex
validate_regex("abc123", r'^[a-z]+\d+$') # OK
validate_regex("ABC123", r'^[a-z]+\d+$') # Raises ValidationError
validate_choice: Validate that a value is in a list of choices.
from varlord.validators import validate_choice
validate_choice("red", ["red", "green", "blue"]) # OK
validate_choice("yellow", ["red", "green", "blue"]) # Raises ValidationError
validate_not_empty: Validate that a value is not empty.
from varlord.validators import validate_not_empty
validate_not_empty("hello") # OK
validate_not_empty(0) # OK (0 is not considered empty)
validate_not_empty(False) # OK (False is not considered empty)
validate_not_empty("") # Raises ValidationError
validate_not_empty([]) # Raises ValidationError
Numeric Validators¶
validate_positive: Validate that a number is positive (> 0).
from varlord.validators import validate_positive
validate_positive(10) # OK
validate_positive(-5) # Raises ValidationError
validate_non_negative: Validate that a number is non-negative (>= 0).
from varlord.validators import validate_non_negative
validate_non_negative(0) # OK
validate_non_negative(10) # OK
validate_non_negative(-5) # Raises ValidationError
validate_integer: Validate that a value is an integer.
from varlord.validators import validate_integer
validate_integer(42) # OK
validate_integer(42.5) # Raises ValidationError
validate_float: Validate that a value is a float or can be converted to float.
from varlord.validators import validate_float
validate_float(3.14) # OK
validate_float(42) # OK (int can be float)
validate_percentage: Validate that a number is a valid percentage (0-100).
from varlord.validators import validate_percentage
validate_percentage(50) # OK
validate_percentage(150) # Raises ValidationError
validate_port: Validate that a number is a valid port number (1-65535).
from varlord.validators import validate_port
validate_port(8080) # OK
validate_port(70000) # Raises ValidationError
validate_greater_than: Validate that a number is greater than a threshold.
from varlord.validators import validate_greater_than
validate_greater_than(10, 5) # OK
validate_greater_than(3, 5) # Raises ValidationError
validate_less_than: Validate that a number is less than a threshold.
from varlord.validators import validate_less_than
validate_less_than(3, 5) # OK
validate_less_than(10, 5) # Raises ValidationError
String Validators¶
validate_length: Validate that a string has a length within a range.
from varlord.validators import validate_length
validate_length("hello", min_length=3, max_length=10) # OK
validate_length("hi", min_length=3) # Raises ValidationError
validate_email: Validate that a string is a valid email address.
from varlord.validators import validate_email
validate_email("user@example.com") # OK
validate_email("invalid-email") # Raises ValidationError
validate_url: Validate that a string is a valid URL.
from varlord.validators import validate_url
validate_url("https://example.com") # OK
validate_url("example.com", require_scheme=False) # OK
validate_url("not-a-url") # Raises ValidationError
validate_ipv4: Validate that a string is a valid IPv4 address.
from varlord.validators import validate_ipv4
validate_ipv4("192.168.1.1") # OK
validate_ipv4("256.1.1.1") # Raises ValidationError
validate_ipv6: Validate that a string is a valid IPv6 address.
from varlord.validators import validate_ipv6
validate_ipv6("2001:0db8::1") # OK
validate_ipv6("192.168.1.1") # Raises ValidationError
validate_ip: Validate that a string is a valid IPv4 or IPv6 address.
from varlord.validators import validate_ip
validate_ip("192.168.1.1") # OK (IPv4)
validate_ip("2001:db8::1") # OK (IPv6)
validate_domain: Validate that a string is a valid domain name.
from varlord.validators import validate_domain
validate_domain("example.com") # OK
validate_domain("sub.example.com") # OK
validate_domain("invalid..domain") # Raises ValidationError
validate_phone: Validate that a string is a valid phone number.
from varlord.validators import validate_phone
validate_phone("+1234567890") # OK (generic)
validate_phone("13800138000", country="CN") # OK (Chinese mobile)
validate_phone("5552345678", country="US") # OK (US phone)
validate_uuid: Validate that a string is a valid UUID.
from varlord.validators import validate_uuid
validate_uuid("550e8400-e29b-41d4-a716-446655440000") # OK
validate_uuid("invalid-uuid") # Raises ValidationError
validate_base64: Validate that a string is valid Base64 encoded data.
from varlord.validators import validate_base64
validate_base64("SGVsbG8gV29ybGQ=") # OK
validate_base64("invalid!") # Raises ValidationError
validate_json_string: Validate that a string is valid JSON.
from varlord.validators import validate_json_string
validate_json_string('{"key": "value"}') # OK
validate_json_string("invalid json") # Raises ValidationError
validate_date_format: Validate that a string matches a date format.
from varlord.validators import validate_date_format
validate_date_format("2024-01-15", "%Y-%m-%d") # OK
validate_date_format("01/15/2024", "%m/%d/%Y") # OK
validate_time_format: Validate that a string matches a time format.
from varlord.validators import validate_time_format
validate_time_format("14:30:00", "%H:%M:%S") # OK
validate_time_format("2:30 PM", "%I:%M %p") # OK
validate_datetime_format: Validate that a string matches a datetime format.
from varlord.validators import validate_datetime_format
validate_datetime_format("2024-01-15 14:30:00") # OK (default format)
validate_datetime_format("2024-01-15 14:30:00", "%Y-%m-%d %H:%M:%S") # OK
Collection Validators¶
validate_list_length: Validate that a list has a length within a range.
from varlord.validators import validate_list_length
validate_list_length([1, 2, 3], min_length=2, max_length=5) # OK
validate_list_length([1], min_length=2) # Raises ValidationError
validate_dict_keys: Validate that a dictionary has required keys and/or only allowed keys.
from varlord.validators import validate_dict_keys
validate_dict_keys({"a": 1, "b": 2}, required_keys=["a"]) # OK
validate_dict_keys({"a": 1}, required_keys=["a", "b"]) # Raises ValidationError
validate_dict_keys({"a": 1, "c": 3}, allowed_keys=["a", "b"]) # Raises ValidationError
File/Path Validators¶
validate_file_path: Validate that a string is a valid file path.
from varlord.validators import validate_file_path
validate_file_path("/path/to/file.txt") # OK
validate_file_path("/nonexistent.txt", must_exist=True) # Raises ValidationError if file doesn't exist
validate_directory_path: Validate that a string is a valid directory path.
from varlord.validators import validate_directory_path
validate_directory_path("/path/to/dir") # OK
validate_directory_path("/nonexistent", must_exist=True) # Raises ValidationError if directory doesn't exist
Complete Example¶
Here’s a complete example using multiple validators:
from dataclasses import dataclass
from varlord.validators import (
validate_range,
validate_email,
validate_url,
validate_port,
validate_length,
validate_not_empty,
)
from dataclasses import dataclass, field
@dataclass(frozen=True)
class AppConfig:
host: str = field(default="0.0.0.0")
port: int = field(default=8000)
admin_email: str = field(default="admin@example.com")
api_url: str = field(default="https://api.example.com")
api_key: str = field(default="")
def __post_init__(self):
validate_not_empty(self.host)
validate_port(self.port)
validate_email(self.admin_email)
validate_url(self.api_url)
validate_length(self.api_key, min_length=32, max_length=64)
Custom Validators¶
Create custom validators:
from varlord.validators import validate_custom, ValidationError
def validate_port(value):
if not (1024 <= value <= 65535):
raise ValidationError("port", value, "must be between 1024 and 65535")
@dataclass(frozen=True)
class AppConfig:
port: int = 8000
def __post_init__(self):
validate_custom(self.port, validate_port)
Or raise ValidationError directly:
from varlord.validators import ValidationError
@dataclass(frozen=True)
class AppConfig:
port: int = 8000
def __post_init__(self):
if not (1024 <= self.port <= 65535):
raise ValidationError(
"port",
self.port,
"must be between 1024 and 65535"
)
Nested Configuration Validation¶
For nested configurations, each nested dataclass should have its own __post_init__
method. Validation is performed automatically when nested objects are created.
from dataclasses import dataclass
from varlord.validators import validate_range, validate_not_empty, validate_email, ValidationError
from dataclasses import dataclass, field
@dataclass(frozen=True)
class DBConfig:
host: str = field(default="localhost")
port: int = field(default=5432)
max_connections: int = field(default=10)
def __post_init__(self):
"""Validate database configuration."""
validate_not_empty(self.host)
validate_port(self.port)
validate_range(self.max_connections, min=1, max=100)
@dataclass(frozen=True)
class AppConfig:
host: str = field(default="0.0.0.0")
port: int = field(default=8000)
db: DBConfig = field(default_factory=lambda: DBConfig())
def __post_init__(self):
"""Validate application configuration."""
# Validate flat fields
validate_range(self.port, min=1, max=65535)
# Nested dataclasses are automatically validated
# DBConfig's __post_init__ is called when DBConfig is instantiated
# No need to manually validate self.db here - it's already validated!
# Optional: Cross-field validation
if self.db is not None:
# Example: Validate that port doesn't conflict with DB port
if self.port == self.db.port:
raise ValidationError(
"port",
self.port,
f"App port conflicts with DB port {self.db.port}"
)
Key Points:
Automatic nested validation: When a nested dataclass is created, its
__post_init__is automatically called. You don’t need to manually validate nested objects in the parent’s__post_init__.Cross-field validation: You can validate relationships between fields (including nested fields) in the parent’s
__post_init__.Validation order: Nested objects are validated first (when created), then the parent object is validated.
Validation Errors¶
Varlord provides several exception types for different validation scenarios:
Value Validation Errors (from varlord.validators):
Value validation errors raise ValidationError with detailed information:
from varlord.validators import ValidationError
try:
app = cfg.load()
except ValidationError as e:
print(f"Key: {e.key}")
print(f"Value: {e.value}")
print(f"Error: {e.message}")
Error Information:
e.key: The configuration key that failed validation (e.g.,"port","db.host")e.value: The invalid valuee.message: Human-readable error message
Model and Structure Validation Errors (from varlord.model_validation):
ModelDefinitionError: Raised when a field is missing required/optional metadataRequiredFieldError: Raised when required fields are missing from configuration -e.missing_fields: List of missing field keys -e.model_name: Name of the model class - Includes comprehensive source mapping help in error message
For more details, see API Reference.
Validator Reference¶
For a complete list of all available validators, see the API Reference.