Skip to content

Validators pubilc API #1422

@mbant

Description

@mbant

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 extending 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions