Validation allows to check that data is consistent. It is run on raw data before it is deserialized. E.g. DateTime deserializes string to datetime.datetime so validations are run on a string before it is parsed. In Object validations are run on a dictionary of fields but after fields themselves were already deserialized. So if you had a field of type DateTime your validator will get a dictionary with datetime object.

Validators are just callable objects that take one or two arguments (first is the data to be validated, second (optional) is the operation context) and raise ValidationError in case of errors. Return value of validator is always ignored.

To add validator or validators to a type, you pass them to type contructor’s validate argument:

def is_odd(data):
    if data % 2 == 0:
        raise ValidationError('Value should be odd')

MyNumber = Integer(validate=is_odd)
MyNumber.load(1)  # => returns 1
MyNumber.load(2)  # => raises ValidationError('Value should be odd')

In simple cases you can create a Predicate validator for which you need to specify a boolean function and error message:

is_odd = Predicate(lambda x: x % 2 != 0, 'Value should be odd')

MyNumber = Integer(validate=is_odd)

In more complex cases where you need to parametrize validator with some data it is more convenient to create a validator class:

from lollipop.validators import Validator

class GreaterThan(Validator):
    default_error_messages = {
        'greater': 'Value should be greater than {value}'

    def __init__(self, value, **kwargs):
        super(GreaterThan, self).__init__(**kwargs)
        self.value = value

    def __call__(self, data):
        if data <= self.value:
            self._fail('greater', data=data, value=self.value)

The last example demonstrates how you can support customizing error messages in your validators: there is a default error message keyed with string ‘greater’ and users can override it when creating validator with supplying new set of error messages in validator constructor:

message = 'Should be greater than answer to the Ultimate Question of Life, the Universe, and Everything'
Integer(validate=GreaterThan(42, error_messages={'greater': message}))

Accumulating Errors

If you writing a whole-object validator that checks various field combinations for correctness, it might be hard to accumulate errors. That’s why the library provides a special builder for errors - ValidationErrorBuilder:

def validate_my_object(data):
    builder = ValidationErrorBuilder()

    if data['foo']['bar'] >= data['baz']['bam']:
        builder.add_error('': 'Should be less than bam')
    if data['foo']['quux'] >= data['baz']['bam']:
        builder.add_error('foo.quux': 'Should be less than bam')