Skip to content

Conversation

wodesuck
Copy link

@wodesuck wodesuck commented Oct 27, 2024

Run user-defined hooks after validate.

One usage for me is clearing default keys when validate error in oneOf/anyOf. For example, if we use extend_with_default from docs to validate oneOf schema with default, such as following code:

from jsonschema import Draft202012Validator, validators


def extend_with_default(validator_class):
    validate_properties = validator_class.VALIDATORS["properties"]

    def set_defaults(validator, properties, instance, schema):
        for property, subschema in properties.items():
            if "default" in subschema:
                instance.setdefault(property, subschema["default"])

        for error in validate_properties(
            validator, properties, instance, schema,
        ):
            yield error

    return validators.extend(
        validator_class, {"properties" : set_defaults},
    )


DefaultValidatingValidator = extend_with_default(Draft202012Validator)

obj = {'type': 'second_type'}
schema = {
    'oneOf': [
        {
            'properties': {
                'type': {'const': 'first_type'},
                'foo': {'default': 'bar'},
            },
            'additionalProperties': False,
        },
        {
            'properties': {
                'type': {'const': 'second_type'},
                'baz': {'default': 'qux'},
            },
            'additionalProperties': False,
        },
    ],
}

DefaultValidatingValidator(schema).validate(obj)

We will get an error like this:

jsonschema.exceptions.ValidationError: {'type': 'second_type', 'foo': 'bar', 'baz': 'qux'} is not valid under any of the given schemas

Failed validating 'oneOf' in schema:
    {'oneOf': [{'properties': {'type': {'const': 'first_type'},
                               'foo': {'default': 'bar'}},
                'additionalProperties': False},
               {'properties': {'type': {'const': 'second_type'},
                               'baz': {'default': 'qux'}},
                'additionalProperties': False}]}

On instance:
    {'type': 'second_type', 'foo': 'bar', 'baz': 'qux'}

That is because the "foo" was added when validate "first_type", making the schema of "second_type" invalid. With validate_hooks, we could add a hook to delete "foo" when validate error on "first_type". For example:

def extend_with_default(validator_class):
    validate_properties = validator_class.VALIDATORS["properties"]
    default_keys = {}

    def set_defaults(validator, properties, instance, schema):
        dkeys = []
        for property, subschema in properties.items():
            if "default" in subschema and property not in instance:
                instance[property] = subschema["default"]
                dkeys.append(property)
        if dkeys:
            default_keys[(id(instance), id(schema))] = dkeys

        for error in validate_properties(
            validator, properties, instance, schema,
        ):
            yield error

    def validate_hook(is_valid, instance, schema):
        dkeys = default_keys.pop((id(instance), id(schema)), [])
        if not is_valid:
            for keys in dkeys:
                del instance[keys]

    return validators.extend(
        validator_class, {"properties" : set_defaults},
        validate_hooks = [validate_hook],
    )

📚 Documentation preview 📚: https://python-jsonschema--1310.org.readthedocs.build/en/1310/

@Julian
Copy link
Member

Julian commented Oct 27, 2024

Hi there. Thanks for your contribution.
It's good to discuss these sorts of changes ahead of time.
I wouldn't want to touch any of this kind of functionality until we support JSON Schema annotations, and possibly until we rewrite the validator interface to support them.
I also wouldn't want to do this to specifically support the default code, as that was entirely a POC to satisfy simple needs from people looking to do so, we'd need multiple use cases for this functionality I think.
(This also would need tests).
Appreciate your contribution though and definitely happy to discuss somewhere.

@Julian Julian closed this Oct 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants