Nested Validation Example

This example demonstrates validation with nested configuration structures.

Source Code

  1"""
  2Example demonstrating validation with nested configuration.
  3
  4This example shows:
  5- Nested dataclass structures (best practice)
  6- Validation at multiple levels
  7- Cross-field validation
  8
  9Run with:
 10    python nested_validation_example.py
 11    python nested_validation_example.py -cv  # Check variables
 12"""
 13
 14import os
 15import sys
 16from dataclasses import dataclass, field
 17
 18from varlord import Config, sources
 19from varlord.validators import ValidationError, validate_not_empty, validate_range, validate_regex
 20
 21# Set environment variables for testing
 22os.environ["DB__HOST"] = "localhost"
 23os.environ["DB__PORT"] = "5432"
 24os.environ["API__TIMEOUT"] = "30"
 25
 26
 27@dataclass(frozen=True)
 28class DBConfig:
 29    """Database configuration."""
 30
 31    host: str = field(default="127.0.0.1", metadata={"description": "Database host"})
 32    port: int = field(default=5432, metadata={"description": "Database port"})
 33    max_connections: int = field(default=10, metadata={"description": "Maximum connections"})
 34
 35    def __post_init__(self):
 36        """Validate database configuration."""
 37        validate_not_empty(self.host)
 38        validate_range(self.port, min=1, max=65535)
 39        validate_range(self.max_connections, min=1, max=100)
 40
 41
 42@dataclass(frozen=True)
 43class APIConfig:
 44    """API configuration."""
 45
 46    timeout: int = field(default=30, metadata={"description": "Request timeout in seconds"})
 47    retries: int = field(default=3, metadata={"description": "Number of retries"})
 48    base_url: str = field(
 49        default="https://api.example.com", metadata={"description": "API base URL"}
 50    )
 51
 52    def __post_init__(self):
 53        """Validate API configuration."""
 54        validate_range(self.timeout, min=1, max=300)
 55        validate_range(self.retries, min=0, max=10)
 56        validate_regex(self.base_url, r"^https?://.+")
 57
 58
 59@dataclass(frozen=True)
 60class AppConfig:
 61    """Application configuration with nested structures."""
 62
 63    host: str = field(default="0.0.0.0", metadata={"description": "Server host address"})
 64    port: int = field(default=8000, metadata={"description": "Server port number"})
 65    # Use default_factory for nested dataclasses (best practice)
 66    db: DBConfig = field(
 67        default_factory=DBConfig, metadata={"description": "Database configuration"}
 68    )
 69    api: APIConfig = field(default_factory=APIConfig, metadata={"description": "API configuration"})
 70
 71    def __post_init__(self):
 72        """Validate application configuration."""
 73        # Validate flat fields
 74        validate_not_empty(self.host)
 75        validate_range(self.port, min=1, max=65535)
 76
 77        # Nested dataclasses are automatically validated when they are created
 78        # DBConfig's __post_init__ and APIConfig's __post_init__ are called automatically
 79        # No need to manually validate self.db or self.api here
 80
 81        # Cross-field validation example
 82        # Validate that API timeout is reasonable compared to DB connection pool
 83        if self.db is not None and self.api is not None:
 84            if self.api.timeout > self.db.max_connections * 10:
 85                raise ValidationError(
 86                    "api.timeout",
 87                    self.api.timeout,
 88                    f"API timeout ({self.api.timeout}s) is too large compared to DB max_connections ({self.db.max_connections})",
 89                )
 90
 91
 92def main():
 93    """Main function."""
 94    cfg = Config(
 95        model=AppConfig,
 96        sources=[
 97            sources.Env(),  # Model defaults applied automatically, model auto-injected
 98            sources.CLI(),  # CLI arguments can override env vars
 99        ],
100    )
101
102    # Handle CLI commands
103    cfg.handle_cli_commands()
104
105    try:
106        app = cfg.load()
107        print("✅ Configuration loaded and validated successfully!")
108        print(f"   Host: {app.host}:{app.port}")
109        print(f"   DB: {app.db.host}:{app.db.port} (max_conn={app.db.max_connections})")
110        print(f"   API: {app.api.base_url} (timeout={app.api.timeout}s, retries={app.api.retries})")
111    except ValidationError as e:
112        print(f"❌ Validation error: {e.key} = {e.value}")
113        print(f"   {e.message}")
114        sys.exit(1)
115    except Exception as e:
116        print(f"❌ Error: {e}")
117        sys.exit(1)
118
119
120if __name__ == "__main__":
121    main()

Key Points

  1. Nested Validation: Each nested dataclass (DBConfig, APIConfig) has its own __post_init__ method for validation.

  2. Automatic Validation: When nested objects are created, their __post_init__ methods are automatically called. You don’t need to manually validate nested objects in the parent’s __post_init__.

  3. Cross-Field Validation: The parent AppConfig.__post_init__ can validate relationships between fields, including nested fields.

  4. Error Handling: Validation errors provide detailed information about which key failed and why.

Running the Example

python examples/nested_validation_example.py

Expected Output

Config loaded successfully:
  Host: 0.0.0.0:8000
  DB: localhost:5432 (max_conn=10)
  API: https://api.example.com (timeout=30s, retries=3)