Skip to content

Custom Validators

Extend PyCharter's validation with custom coercion and validation rules.

Custom Coercion

Coercions transform data before validation:

from pycharter.shared.coercions import register_coercion

def coerce_phone_number(value):
    """Extract digits from phone number."""
    if value is None:
        return None
    if isinstance(value, str):
        return ''.join(c for c in value if c.isdigit())
    return str(value)

register_coercion("coerce_phone_number", coerce_phone_number)

Use in schema:

properties:
  phone:
    type: string
    coercion: coerce_phone_number

Custom Validation

Validations check data after schema validation:

from pycharter.shared.validations import register_validation

def is_valid_phone(min_digits=10, max_digits=15):
    """Validate phone number length."""
    def _validate(value, info):
        if value is None:
            return value

        digits = ''.join(c for c in str(value) if c.isdigit())
        if len(digits) < min_digits:
            raise ValueError(f"Phone must have at least {min_digits} digits")
        if len(digits) > max_digits:
            raise ValueError(f"Phone must have at most {max_digits} digits")

        return value
    return _validate

register_validation("is_valid_phone", is_valid_phone)

Use in schema:

properties:
  phone:
    type: string
    validations:
      is_valid_phone:
        min_digits: 10
        max_digits: 15

Validation Factory Pattern

Custom validations must follow the factory pattern:

def my_validation(param1, param2=default):
    """Factory that returns the actual validator."""
    def _validate(value, info):
        # Validation logic here
        if not is_valid(value, param1, param2):
            raise ValueError("Validation failed")
        return value
    return _validate

Complex Example: Credit Card Validation

from pycharter.shared.coercions import register_coercion
from pycharter.shared.validations import register_validation

# Coercion: extract digits
def coerce_credit_card(value):
    if value is None:
        return None
    return ''.join(c for c in str(value) if c.isdigit())

register_coercion("coerce_credit_card", coerce_credit_card)

# Validation: Luhn algorithm
def is_valid_credit_card():
    def _validate(value, info):
        if value is None:
            return value

        digits = [int(d) for d in str(value)]
        checksum = 0

        for i, digit in enumerate(reversed(digits)):
            if i % 2 == 1:
                digit *= 2
                if digit > 9:
                    digit -= 9
            checksum += digit

        if checksum % 10 != 0:
            raise ValueError("Invalid credit card number")

        return value
    return _validate

register_validation("is_valid_credit_card", is_valid_credit_card)

Best Practices

  1. Handle None values - Always check for None early
  2. Return the value - Validators must return the (possibly modified) value
  3. Use clear error messages - Help users understand what went wrong
  4. Keep coercions idempotent - Running twice should give same result
  5. Document your rules - Add docstrings explaining behavior

See Also