Custom Validators - PyCharter
Description: Data Contract Management, ETL Pipelines, and Quality Assurance for Python

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

- Contracts Tutorial
- Validator API Reference