# Custom Validators

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

## Custom Coercion

Coercions transform data before validation:

```python
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:

```yaml
properties:
  phone:
    type: string
    coercion: coerce_phone_number
```

## Custom Validation

Validations check data after schema validation:

```python
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:

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

## Validation Factory Pattern

Custom validations must follow the factory pattern:

```python
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

```python
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](../tutorials/contracts-validation.md)
- [Validator API Reference](../api/validator.md)
