Cerberus Validation

from cerberus import Validator

# 1. Define expectations
schema = {
    'host': {'type': 'string', 'required': True},
    'port': {'type': 'integer', 'min': 80, 'max': 9000, 'default': 8080}
}

# 2. Instantiate and clean/validate
v = Validator(schema, purge_unknown=True)
config = {'host': 'localhost', 'port': 443, 'extra_key': 'drop_me'}

if v.validate(config):
    clean_config = v.document  # Contains defaults and removes 'extra_key'
else:
    print(v.errors)

🛠️ Essential Validator Options

  • Pass these arguments to Validator() to control config parsing behaviour:
    • purge_unknown=True: Silently drops keys not defined in the schema.
    • allow_unknown=True: Keeps keys not defined in the schema without failing.
    • require_all=True: Makes every schema rule implicitly required: True.

Common Schema

Basic Data Types

schema = {
    'app_name': {'type': 'string', 'empty': False},
    'workers': {'type': 'integer', 'coerce': int},
    'timeout': {'type': 'float', 'min': 0.1},
    'debug_mode': {'type': 'boolean'}
}

Allowed Values & Regex

schema = {
    'environment': {'type': 'string', 'allowed': ['dev', 'staging', 'prod']},
    'log_level': {'type': 'string', 'regex': '^(INFO|WARN|ERROR)$'}
}

Dictionaries (Nested Configs)

  • Use schema recursively for subsections like database or API blocks.
schema = {
    'database': {
        'type': 'dict',
        'required': True,
        'schema': {
            'host': {'type': 'string', 'required': True},
            'port': {'type': 'integer'}
        }
    }
}

Lists & Sequences

  • Use schema for uniform items, or items for fixed-length tuples.
schema = {
    'allowed_ips': {'type': 'list', 'schema': {'type': 'string'}},
    'geo_coordinates': {'type': 'list', 'items': [{'type': 'float'}, {'type': 'float'}]}
}

🔄 Dynamic Modifiers (Coercion & Defaults)

  • Cerberus can modify your data during validation.
  • Important: Always read the processed data from v.document, not your input variable.
schema = {
    # Sets value if missing from config
    'port': {'type': 'integer', 'default': 8080}, 
    
    # Converts types (e.g., handles env variables loaded as strings)
    'retries': {'type': 'integer', 'coerce': int}, 
    
    # Cleans strings automatically
    'api_key': {'type': 'string', 'coerce': lambda s: s.strip()} 
}

🎛️ Advanced Control Flow

Dependencies

  • Field validation depends on the state of another field.
schema = {
    'use_ssl': {'type': 'boolean'},
    'ssl_cert': {'type': 'string', 'dependencies': 'use_ssl'} # Only required if use_ssl is True
}

Alternative Choices (anyof, allof, oneof)

  • Perfect for flexible configuration structures.
schema = {
    'bind_address': {
        'anyof': [
            {'type': 'string', 'regex': r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'}, # IPv4
            {'type': 'string', 'allowed': ['localhost']}
        ]
    }
}

🪚 Custom Validators & Rules

  • Extend the Validator class to write custom business logic (e.g., testing if a file path actually exists).
import os
from cerberus import Validator

class ConfigValidator(Validator):
    def _validate_is_existing_file(self, is_existing_file, field, value):
        """
        The rule's arguments are validated against this schema:
        {'type': 'boolean'}
        """
        if is_existing_file and not os.path.isfile(value):
            self._error(field, f"Path '{value}' does not exist or is not a file.")

# Usage
schema = {'config_path': {'type': 'string', 'is_existing_file': True}}
v = ConfigValidator(schema)
from cerberus import Validator

class ConfigValidator(Validator):
    #                     ┌── 'is_existing_file' rule argument (e.g., True)
    #                     │               ┌── Current field name (e.g., 'config_path')
    #                     │               │       ┌── The derived value from your data
    #                     ▼               ▼       ▼
    def _validate_is_existing_file(self, is_existing_file, field, value):
        print(f"Checking field '{field}' with value: {value}")


schema = {
    'config_path': {'type': 'string', 'is_existing_file': True}
}

# 1. Instantiate the validator with your rules
v = ConfigValidator(schema)

# 2. Define your input data
config_data = {
    'config_path': '/etc/settings.conf'  # <── This is where 'value' comes from!
}

# 3. Trigger the validation loop
v.validate(config_data) 
# Output: Checking field 'config_path' with value: /etc/settings.conf

🧠 How Cerberus Maps This Behind the Scenes

  1. v.validate(config_data) tells Cerberus to look at the config_data dictionary.
  2. It processes the key config_path.
  3. It sees the custom rule is_existing_file: True in the schema.
  4. It calls your method _validate_is_existing_file and automatically maps the arguments:
    1. is_existing_file = True (from the schema)
    2. field = 'config_path' (the key name)
    3. value = '/etc/settings.conf' (the key’s value in your data)