Search code examples
pythonvalidationcerberus

cerberus: Validate an optional field occurs at least once


I'm using cerberus to validate data. One of my fields is optional - it doesn't need to be present for every item. However, the key must be populated at least once across the entire data array.

As an example, say I want to validate the key 'c' occurs in at least one dictionary in my data list:

from cerberus import Validator

has_c = {'data': [{'a': 1, 'b': 2}, {'b': 2}, {'c': 3}]}
no_c = {'data': [{'a': 1, 'b': 2}, {'a': 1}]}

schema = {'data':
          {'type': 'list',
           'schema': {
               'type': 'dict',
               'schema': {
                   'a': {'required': True},
                   'b': {'required': True},
                   'c': {'required': False, 'at_least_one': True}
               }
           }
           }
          }

class MyValidator(Validator) # Some fancy code...
....

v = MyValidator()

v.validate(has_c, schema) # Passes
v.validate(no_c, schema) # Fails

This seems doable outside of cerberus, but I'd like to keep the method in my validator if possible.


Solution

  • If you want the method to be in the Validator subclass, then you will want to create a custom rule just like you were thinking.

    from cerberus import Validator
    
    test_with_c = {'data': [{'a': 1, 'b': 2}, {'b': 2}, {'c': 3}]}
    test_with_no_c = {'data': [{'a': 1, 'b': 2}, {'a': 1}]}
    
    class MyValidator(Validator):
        def _validate_has_c(self, has_c, field, value):
            seen_c = False
            for v in value:
                if "c" in v:
                    seen_c = True
            if has_c and not seen_c:
                self._error(field, "Must contain a 'c' key")
    
    schema = {
        "data": {
            "type": "list",
            "has_c": True
        }
    }
    
    v = MyValidator(schema)
    
    print(v(test_with_c), v.errors)
    print(v(test_with_no_c), v.errors)
    

    Running this will yield the results you want with respect to looking for a c key in one of the elements. Running that code yields

    True {}
    False {'data': ["Must contain a 'c' key"]}