-
-
Notifications
You must be signed in to change notification settings - Fork 595
Description
Hello, I'm struggling to enforce some custom validation on properties through extending a Validator and maybe someone can help me out or clarify how extend
ing should work. Note that I want to add validation for a JSONSchema authored by a user - so it's more "meta validation".
Essentially I want to do something like enforcing that if a property is marked as "required" it should not specify a default, and conversely if it specify a default it should not appear in the required array at its level".
I can get something that seems to be working at a high level below:
from jsonschema import Draft202012Validator, validators, exceptions
from jsonschema.protocols import Validator
validate_properties = Draft202012Validator.VALIDATORS["properties"]
def custom_properties_validator(validator, properties, instance, schema):
# Maintain old behaviour
yield from validate_properties(validator, properties, instance, schema)
# Extend behaviour
if not validator.is_type(instance, "object"):
return
current_properties = instance.get("properties", {})
required_properties = instance.get("required", [])
for prop_name, prop_schema in current_properties.items():
is_required = prop_name in required_properties
has_default = "default" in prop_schema
if not is_required and not has_default:
yield exceptions.ValidationError(
f"Primitive property '{prop_name}' is not required and must have a 'default' value."
)
if has_default and is_required:
yield exceptions.ValidationError(
f"Primitive property '{prop_name}' has a 'default' value and must not be in the 'required' list."
)
if __name__ == "__main__":
user_schema = {
"type": "object",
"title": "test",
"properties": {
"test": {"type": "string"},
},
"required": []
}
my_validator: Validator = validators.extend(
Draft202012Validator,
{"properties": custom_properties_validator},
)
errors = list(my_validator(Draft202012Validator.META_SCHEMA).iter_errors(user_schema))
print(f"Found {len(errors)} validation errors:")
for i, error in enumerate(errors, 1):
path_str = " -> ".join(map(str, error.path))
print(f" {i}. JSONPath: {error.json_path}")
print(f" {i}. Path: {path_str}")
print(f" Error: {error.message}")
But there's a big caveat as in I can't really know what's the path of the property I am failing to validate!
I can kinda start to cheat my way into it by doing something more like
if not is_required and not has_default:
for err in validator.descend(
required_properties,
{"contains": {"const": prop_name}},
path="properties",
):
err.message = f"'{prop_name}' is not required and must have a 'default' value."
yield err
in the custom validation steps, but
- I can only descend one level - maybe I can call it twice in this case but it will seem even hackyer
- I am using descend, which is not documented in the validator protocol public API
Is my only way around this implementing the schema-walking part myself so that I can keep track of my own custom errors? Or am I simply making use of the library in an improper way?