2

I'm using Cerberus to validate the data posted as JSON to a Flask-based ReST-API. I want at least one of the two fields freight_id and tender_id to be present.

These mappings would be considered as valid:

{"freight_id": 1, "price" : 12000}

{"tender_id": 1, "price": 12000}

{"freight_id": 1, "tender_id" : 2, "price": 12000}

While this one would not:

{"price": 12000}

How can I formulate a schema for such validation with Cerberus?

I almost read all documentation, but did not find any answer. The excludes-rule doesn't fit my need.

funky-future
  • 3,716
  • 1
  • 30
  • 43
Shahryar Saljoughi
  • 2,599
  • 22
  • 41

2 Answers2

4

Using cerberus 1.0, you can implement this using oneof rule in its agglutinating form as in this documentation example. With this, you can validate against different schemas of which exactly one must validate:

The downside is that you may need an additional level on you dict, like price below:

First schema, freight and price:

>>> schema_1 = {'freight_id': {'type': 'integer', 'required': True},
...             'price': {'type': 'integer', 'required': True}}

Second schema, tender and price:

>>> schema_2 = {'tender_id': {'type': 'integer', 'required': True},
...             'price': {'type': 'integer', 'required': True}}

Third schema, freight, tender and price:

>>> schema_3 = {'tender_id': {'type': 'integer', 'required': True},
...             'freight_id': {'type': 'integer', 'required': True},
...             'price': {'type': 'integer', 'required': True}}

Putting these together:

>>> from cerberus import Validator
>>>
>>> price_validator = Validator(
...     {'price': {'type': 'dict', 
...                'oneof_schema': [schema_1, schema_2, schema_3]}})

The results:

>>> price_validator.validate({"price": {"freight_id": 1, "price" : 12000}}) 
True
>>> price_validator.validate({"price": {"tender_id": 2, "price" : 12000}})
True
>>> price_validator.validate(
...     {"price": {"freight_id": 1, "tender_id": 2, "price": 1200}}) 
True

>>> price_validator.validate({"price": {"freight_id": 1, "tender_id": 2}}) 
False 
>>> price_validator.validate({"price": {"price" : 12000}})
False
gcw
  • 1,639
  • 1
  • 18
  • 37
4

@gcw's solution can be made even shorter:

from cerberus import Validator, rules_set_registry

required_integer = {'type': 'integer', 'required': True}

schemas = (
    {'freight_id': required_integer, 'price': required_integer},
    {'tender_id': required_integer, 'price': required_integer},
    {'freight_id': required_integer, 'tender_id': required_integer,
     'price': required_integer},
)

As the three schemas are mutually exclusive anyway, there's no need to use the one_of rule, simply test whether one matches:

validator = Validator()
valid = any(validator(document, schema) for schema in schemas)
funky-future
  • 3,716
  • 1
  • 30
  • 43
  • Is there maybe a way to perform the same thing with Cerberus < 1.0? – errata Apr 11 '18 at 12:50
  • The trouble with this is that `validator.errors` won't really give an error message that is helpful to the end user. I don't understand why cerberus don't allow a `one_of` for top level elements. – djsumdog Mar 27 '22 at 21:59