Required - A easy multi field validator

tldr; Required is a simple cross (multi) field validator which allows you to express and validate complex dependencies. You can view the source code and some other examples on github.

The Problem

Say you are making a REST API endpoint which takes a large number of optional parameters. All which are valid in different combinations.

/api/search?query=hello&location=(0,0)&radius=1,&categories=1,2,3&start_date=2017-03-01&end_date=2017-03-02

Some basic validation logic just for start_date & end_date can be described as:

1) end_date requires start_date to be present
2) end_date requires start_date to be less than it
3) end_date cannot be greater than 1 year in advance
4) start_date cannot be greater than 1 year in advance

If you are using a REST framework like Django Rest Framework or Flask Rest Plus you will have have a serializer which needs to validate the data / params. This will end end up looking something like:

def clean(self, data):  
    start_date = data.get("start_date")
    end_date = data.get("end_date")
    next_year = datetime.date.today() + relativedelta(years=+1)
    if end_date and end_date > next_year:
        raise ValidationError(...)
    next_year = datetime.date.today() + relativedelta(years=+1)
    if start_date and start_date > next_year:
        raise ValidationError(...)
    if start_date and end_date and start_date > end_date:
        raise ValidationError(...)

While this is acceptable code, it is also very boilerplate, repetitive and doesn't lend itself well to being used outside this context. What if we could do better?

Expressing a dependency graph

What if we simply defined pairwise relationships and the conditions under which they are valid. The four rules for start_date and end_date would then look something like:

rule_1 = Requires("end_date", "start_date")  
# When end_date, require start_date to be present
rule_2 = Requires("end_date", R("end_date") > R("start_date"))  
# when end_date require it to be greater than start date
rule_3 = Requires("end_date", R("end_date") < today.date() + one_year)  
# when end_date, require it to be less than one year in advance
rule_4 = Requires("start_date", R("start_date") < today.date() + one_year)  
# when start_date, require it to be less than one year in advance

We can then combine all the constraints by summing them together:

requirements = rule_1 + rule_2 + rule_3 + rule_4  

The requirements object encapsulates all the validation logic, with the added benefit you can import it, add to it and otherwise treat it as any other first class object.

You could then combine it with other validators eg. a location validator and query validator and therefore compose validators from other basic validators.

Validation

When you finally want to validate the data you have. You can just call the validate method on the requirements object. For the first two rules, it would validate in the following way:

1)

data = {  
    "end_date": yesterday,
}

requirements.validate(data)  # RequirementError: "end_date" requires "start_date" to be present

data = {  
    "start_date": today,
    "end_date": yesterday,
}

requirements.validate(data)  # RequirementError: "end_date" requires "end_date" to be greater than "start_date"  

Your final Serializer should end up looking a lot more readable with much less boilerplate.

class RequestSerializer(serializers.Serializer):

    REQUIREMENTS = (
        Requires("end_date", "start_date") +
        Requires("end_date", R("end_date") > R("start_date")) + 
        Requires("end_date", R("end_date") < today.date() + one_year) + 
        Requires("start_date", R("start_date") < today.date() + one_year)
     )

    start_date = serializers.DateField(required=False)
    end_date = serializers.DateField(required=False)

Pip package

You can view the source code and some other examples on github and it is available in a pip installable package called required.