Search code examples
pythonpython-3.xvalidationcerberus

Python: cerberus check_with function


I would like to validate a dict, where the values adhere to the following rules:

  • value must be either a single float or List(float)
  • if it is a single float, the value must be 1
  • if it's a List(float), each float must be positive

Here's my code and some test assertions, which are working properly:

import cerberus

v = cerberus.Validator()

schema1 = {
    "key1": {
        "type": ["float", "list"],
        "min": 1,
        "max": 1,
        "schema": {"type": "float", "min": 0},
    }
}
document1 = {"key1": 1}
document2 = {"key1": 5}
document3 = {"key1": "5"}
document4 = {"key1": [0.5, 0.3]}
document5 = {"key1": ["0.5", 0.3]}

assert v.validate(document1, schema1)
assert not v.validate(document2, schema1)
assert not v.validate(document3, schema1)
assert v.validate(document4, schema1)
assert not v.validate(document5, schema1)

Now, I have to implement one more condition:

  • if it's a List(float), the sum of float must equal 1

Therefore, I wrote a check_with function as described in the docs (https://docs.python-cerberus.org/en/stable/validation-rules.html).

from cerberus import Validator

class MyValidator(Validator):
    def _check_with_sum_eq_one(self, field, value):
        """Checks if sum equals 1"""

        if sum(value) != 1:
            self._error(field, f"Sum of '{field}' must exactly equal 1")

The adjusted schema and test documents look like this:

v = MyValidator()

schema2 = {
    "key1": {
        "type": ["float", "list"],
        "min": 1,
        "max": 1,
        "schema": {"type": "float", "min": 0, "max": 1, "check_with": "sum_eq_one"},
    }
}

document1 = {"key1": 1}
document2 = {"key1": 5}
document3 = {"key1": "5"}
document4 = {"key1": [0.5, 0.3]}  # error
document5 = {"key1": ["0.5", 0.3]}  # error
document6 = {"key1": [0.5, 0.5]}  # error

Now, whenever the value is a List(float), only the first element of the list will be injected into my function, leading to an TypeError: 'float' object is not iterable.
When validating document4, field will be int=0 and value=0.5. So the error message makes sense.

I am wondering, why the whole list is not passed to my function? What am I missing here?


Solution

  • What if you try to catch the error and only continue your function, if the error was occurred? For example like in this manner:

    class MyValidator(Validator):
    
    def _check_with_sum_eq_one(self, field, value):
        """ Checks whether value is a list and its sum equals 1.0. """
        if isinstance(value, list) and sum(value) != 1.0:
            self._error(str(value), f"Sum of '{field}' must exactly equal 1")
    
    
    schema2 = {
        "key1": {
            "type": ["list", "float"],
            "min": 1,
            "max": 1,
            "schema": {"type": "float", "min": 0, "max": 1},
            "check_with": "sum_eq_one",
        }
    }
    
    v = MyValidator(schema2)
    
    document1 = {"key1": 1}
    document2 = {"key1": 5}
    document3 = {"key1": "5"}
    document4 = {"key1": [0.3, 0.5]}  # error
    document5 = {"key1": ["0.5", 0.3]}  # error
    #document6 = {"key1": [0.5, 0.5]}  # error
    assert v.validate(document1)
    assert not v.validate(document2)
    assert not v.validate(document3)
    assert v.validate(document4)
    assert not v.validate(document5)