Lightweight, extensible schema and data validation tool for Python dictionaries.
Cerberus provides an extensible type system that allows defining custom types for validation. This enables domain-specific validation logic and type constraints beyond the built-in types.
# Import types and utilities for custom type definitions
from cerberus import TypeDefinition, validator_factory
from cerberus.utils import readonly_classproperty
# Platform-specific types used in built-in definitions
from cerberus.platform import Container, Mapping, Sequence, _str_type, _int_typesNamed tuple for defining custom types that can be used in validator type mappings.
TypeDefinition = namedtuple('TypeDefinition', 'name,included_types,excluded_types')
"""
Defines a custom type for validation.
Fields:
- name: Descriptive type name (str)
- included_types: Tuple of allowed Python types
- excluded_types: Tuple of excluded Python types
A value is valid for this type if it's an instance of any type in
included_types and not an instance of any type in excluded_types.
"""Factory function for creating custom validator classes with mixins.
def validator_factory(name, bases=None, namespace={}):
"""
Dynamically create a Validator subclass with mixin capabilities.
Parameters:
- name: Name of the new validator class (str)
- bases: Base/mixin classes to include (tuple, class, or None)
- namespace: Additional class attributes (dict)
Returns:
type: New Validator subclass with combined functionality
Note: Docstrings from mixin classes are automatically combined
if __doc__ is not specified in namespace.
"""Utility classes for advanced property management.
class readonly_classproperty(property):
"""
Creates read-only class properties that raise errors on modification attempts.
Usage:
class MyValidator(Validator):
@readonly_classproperty
def my_property(cls):
return "read-only value"
"""
def __get__(self, instance, owner): ...
def __set__(self, instance, value): ... # Raises RuntimeError
def __delete__(self, instance): ... # Raises RuntimeErrorCerberus provides a comprehensive set of built-in type definitions that map common Python types to validation constraints.
# Built-in types_mapping in Validator class
types_mapping = {
'binary': TypeDefinition('binary', (bytes, bytearray), ()),
'boolean': TypeDefinition('boolean', (bool,), ()),
'container': TypeDefinition('container', (Container,), (_str_type,)),
'date': TypeDefinition('date', (date,), ()),
'datetime': TypeDefinition('datetime', (datetime,), ()),
'dict': TypeDefinition('dict', (Mapping,), ()),
'float': TypeDefinition('float', (float, _int_types), ()),
'integer': TypeDefinition('integer', (_int_types,), ()),
'list': TypeDefinition('list', (Sequence,), (_str_type,)),
'number': TypeDefinition('number', (_int_types, float), (bool,)),
'set': TypeDefinition('set', (set,), ()),
'string': TypeDefinition('string', (_str_type,), ()),
}Type Details:
from cerberus import Validator, TypeDefinition
# Define a custom type for positive numbers
positive_number = TypeDefinition(
'positive_number',
(int, float), # Include integers and floats
() # No excluded types
)
# Add to validator's type mapping
class CustomValidator(Validator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.types_mapping['positive_number'] = positive_number
# Define custom validation method for positive constraint
def _validate_positive(self, positive, field, value):
"""Test if value is positive"""
if positive and value <= 0:
self._error(field, "must be positive")
# Add the method to our custom validator
CustomValidator._validate_positive = _validate_positive
# Use the custom type and validator
schema = {
'count': {'type': 'positive_number', 'positive': True}
}
v = CustomValidator(schema)
print(v.validate({'count': 5})) # True
print(v.validate({'count': -1})) # False
print(v.validate({'count': '5'})) # False (not int or float)from cerberus import TypeDefinition
# Define a type that accepts strings but excludes empty strings
non_empty_string = TypeDefinition(
'non_empty_string',
(str,),
() # We'll handle empty string logic in custom validator
)
# Define a type for numeric types excluding booleans
numeric_no_bool = TypeDefinition(
'numeric_no_bool',
(int, float, complex),
(bool,) # Exclude booleans even though bool is subclass of int
)
# Use in schema
schema = {
'name': {'type': 'non_empty_string'},
'value': {'type': 'numeric_no_bool'}
}from cerberus import validator_factory
from cerberus.validator import Validator
# Define mixin classes
class EmailValidatorMixin:
"""Adds email validation capabilities"""
def _validate_email_domain(self, domain, field, value):
"""Validate email domain"""
if '@' in value and not value.split('@')[1].endswith(domain):
self._error(field, f"must be from {domain} domain")
class NumericValidatorMixin:
"""Adds numeric validation capabilities"""
def _validate_even(self, even, field, value):
"""Validate if number is even"""
if even and value % 2 != 0:
self._error(field, "must be even")
# Create custom validator class with mixins
CustomValidator = validator_factory(
'CustomValidator',
(EmailValidatorMixin, NumericValidatorMixin),
{
'custom_attribute': 'custom_value'
}
)
# Use the custom validator
schema = {
'email': {'type': 'string', 'email_domain': '.com'},
'count': {'type': 'integer', 'even': True}
}
v = CustomValidator(schema)
print(v.validate({
'email': 'user@example.com',
'count': 4
})) # True
print(v.validate({
'email': 'user@example.org', # Wrong domain
'count': 3 # Not even
})) # Falsefrom cerberus import Validator, TypeDefinition
# Create validator with custom types
class ExtendedValidator(Validator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add custom types to existing mapping
self.types_mapping.update({
'phone_number': TypeDefinition('phone_number', (str,), ()),
'uuid': TypeDefinition('uuid', (str,), ()),
'positive_int': TypeDefinition('positive_int', (int,), (bool,))
})
# Define validation methods for custom types
def _validate_phone_format(self, phone_format, field, value):
"""Validate phone number format"""
import re
if phone_format and not re.match(r'^\+?1?\d{9,15}$', value):
self._error(field, "invalid phone number format")
def _validate_uuid_format(self, uuid_format, field, value):
"""Validate UUID format"""
import re
if uuid_format and not re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', value.lower()):
self._error(field, "invalid UUID format")
def _validate_positive(self, positive, field, value):
"""Validate positive number"""
if positive and value <= 0:
self._error(field, "must be positive")
# Add methods to validator
ExtendedValidator._validate_phone_format = _validate_phone_format
ExtendedValidator._validate_uuid_format = _validate_uuid_format
ExtendedValidator._validate_positive = _validate_positive
# Use custom types in schema
schema = {
'id': {'type': 'uuid', 'uuid_format': True},
'phone': {'type': 'phone_number', 'phone_format': True},
'count': {'type': 'positive_int', 'positive': True}
}
v = ExtendedValidator(schema)from cerberus import validator_factory
class LoggingMixin:
"""Adds logging to validation"""
def validate(self, *args, **kwargs):
print(f"Starting validation with schema: {list(self.schema.keys())}")
result = super().validate(*args, **kwargs)
print(f"Validation result: {result}")
return result
class CachingMixin:
"""Adds result caching"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cache = {}
def validate(self, document, *args, **kwargs):
doc_hash = hash(str(sorted(document.items())))
if doc_hash in self._cache:
return self._cache[doc_hash]
result = super().validate(document, *args, **kwargs)
self._cache[doc_hash] = result
return result
# Create validator with multiple mixins
EnhancedValidator = validator_factory(
'EnhancedValidator',
(LoggingMixin, CachingMixin)
)
v = EnhancedValidator({'name': {'type': 'string'}})
v.validate({'name': 'test'}) # Logs and caches result
v.validate({'name': 'test'}) # Uses cached resultfrom cerberus import Validator
from cerberus.utils import readonly_classproperty
class ConfiguredValidator(Validator):
@readonly_classproperty
def default_schema(cls):
return {
'id': {'type': 'integer', 'required': True},
'name': {'type': 'string', 'required': True}
}
@readonly_classproperty
def version(cls):
return "1.0.0"
# Access read-only properties
print(ConfiguredValidator.default_schema) # Works
print(ConfiguredValidator.version) # Works
# Attempting to modify raises RuntimeError
try:
ConfiguredValidator.version = "2.0.0"
except RuntimeError as e:
print(f"Error: {e}") # "This is a readonly class property."from cerberus import Validator, TypeDefinition
class BusinessValidator(Validator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Define business-specific types
self.types_mapping.update({
'currency': TypeDefinition('currency', (int, float), (bool,)),
'percentage': TypeDefinition('percentage', (int, float), (bool,)),
'business_id': TypeDefinition('business_id', (str,), ())
})
# Add business rule validations
def _validate_currency_positive(self, currency_positive, field, value):
if currency_positive and value < 0:
self._error(field, "currency amounts must be positive")
def _validate_percentage_range(self, percentage_range, field, value):
if percentage_range and not (0 <= value <= 100):
self._error(field, "percentage must be between 0 and 100")
def _validate_business_id_format(self, business_id_format, field, value):
if business_id_format and not value.startswith('BIZ'):
self._error(field, "business ID must start with 'BIZ'")
BusinessValidator._validate_currency_positive = _validate_currency_positive
BusinessValidator._validate_percentage_range = _validate_percentage_range
BusinessValidator._validate_business_id_format = _validate_business_id_format
# Use in business domain schemas
product_schema = {
'id': {'type': 'business_id', 'business_id_format': True},
'price': {'type': 'currency', 'currency_positive': True},
'discount': {'type': 'percentage', 'percentage_range': True}
}
v = BusinessValidator(product_schema)
result = v.validate({
'id': 'BIZ123',
'price': 29.99,
'discount': 15
})
print(result) # TrueInstall with Tessl CLI
npx tessl i tessl/pypi-cerberus