keleshev / schema

Schema validation just got Pythonic
MIT License
2.86k stars 214 forks source link

Question - How do I define conditional rules? #286

Open fopguy41 opened 1 year ago

fopguy41 commented 1 year ago

Say I have a dictionary where the keys are conditional. How do I write a schema to check it?

For example, the following 2 are ok.

{
    'name': 'x',
    'x_val': 1
}

{
    'name': 'y',
    'y_val': 1
}

But the following 2 are not

{
    'name': 'x',
    'y_val': 1
}

{
    'name': 'y',
    'x_val': 1
}

The existence of what keys need to be in the dict is conditional on the value of name. I can't just say this is a dict with these keys. Name x has a certain set of keys (in this case, x_val), and name y has a different set of keys (y_val).

Of course, I can write my own lambda function that takes in such a dictionary and performs the checks. But I was wondering if there's some kind of out of the box solution for this type of validation.

thanks

LandingEllipse commented 1 year ago

I'm a new user, so there might be a neater way to take care of conditional rules, but here's an approach based on the Customized Validation section of the readme:

from schema import Schema, SchemaMissingKeyError, Or

class ConditionalSchema(Schema):
    def validate(self, data, _is_conditional_schema=True, **kwargs):
        data = super().validate(data, _is_conditional_schema=False, **kwargs)
        if _is_conditional_schema:
            if data["name"] == "x" and "x_val" not in data:
                raise SchemaMissingKeyError("x_val")
            elif data["name"] == "y" and "y_val" not in data:
                raise SchemaMissingKeyError("y_val")
        return data

sch = ConditionalSchema(
    {
        "name": Or("x", "y"),
        Or("y_val", "x_val", only_one=True): int,
    }
)
sch.validate({"name": "x", "x_val": 1})  # => {'name': 'x', 'x_val': 1}
sch.validate({"name": "y", "y_val": 1})  # => {'name': 'y', 'y_val': 1}
sch.validate({"name": "x", "y_val": 1})  # => schema.SchemaMissingKeyError: x_val
sch.validate({"name": "y", "x_val": 1})  # => schema.SchemaMissingKeyError: y_val

This approach is pretty flexible, but comes at the cost a level of indirection with respect to where the rules are defined.

Note that the passing through of kwargs is only necessary when nesting different Schema subclasses and could have been omitted from the above example.

Cheers!