jsonschema
This guide will show you an example how to create a custom validator, that will add:
- Type float – jsonschema knows by default only type number, which covers all numbers (integers, floating point numbers, complex numbers, …).
- Format even – limiting any number to even numbers only.
- Validator is_positive – accepting only positive numbers when set to True and all the rest if set to False.
Custom jsonschema validator class
To add support for custom type, format, and validator, we must create a new IValidator class. We will base it on Draft4Validator. Then we will create a function that will work as validator for positive numbers, create another function as a format checker for even numbers and lastly, add float as a new type.
We will write the example as a unit test TestCase, so we can easily test at the end that our new validator works as expected.
Schema definition
Let us first add required imports and define the schema that we will use for checking our JSON data:
from numbers import Number # used in custom validator from unittest import TestCase, main # for testing our implementation from jsonschema import validators, Draft4Validator, FormatChecker from jsonschema.exceptions import ValidationError class TestCustomTypeValidatorFormat(TestCase): def test_combination(self): schema = { 'type': 'object', 'properties': { 'value': { 'type': 'float', 'format': 'even', 'is_positive': True }, } }
This schema will accept JSON in format {‘value’: number}, where number is even, positive and float (2.0, 4.0, etc.).
Custom JSON schema validator
Next, we will define a validator, that will validate a (non-)positive number based on its settings. The validator can be set to a Boolean value. Validator must be a function, that accepts exactly 4 arguments in this order:
- validator – instance of validator that is calling this method.
- value – value set to the validator in the schema (in our case True/False).
- instance – value that is being validated (in our case the number).
- schema – the part of schema where this validator is used (in our case the properties/value dict).
def fun(): if cond: yield True
def is_positive(validator, value, instance, schema): if not isinstance(instance, Number): yield ValidationError("%r is not a number" % instance) def is_positive(validator, value, instance, schema): if not isinstance(instance, Number): yield ValidationError("%r is not a number" % instance) if value and instance <= 0: yield ValidationError("%r is not positive integer" % (instance,)) elif not value and instance > 0: yield ValidationError("%r is not negative integer nor zero" % (instance,))
A validator must yield a ValidationError when the validation fails. You can get inspired in jsonschema._validators. Let us add the new validator among existing ones and create a new IValidator class using this validator:
all_validators = dict(Draft4Validator.VALIDATORS) all_validators['is_positive'] = is_positive MyValidator = validators.create( meta_schema=Draft4Validator.META_SCHEMA, validators=all_validators )
Custom JSON schema format checker
Creating a custom format checker again requires defining a function, that will check the format. This method can be the registered either globally for all FormatChecker instances or tied to a single instance. Let us define a format checker to check that a number is even. First, we need a FormatChecker instance. Then we will use this instance as a decorator for the format checking function. The function must accept exactly one argument – the value to check.
format_checker = FormatChecker() @format_checker.checks('even') def even_number(value): return value % 2 == 0
A format checker method can also raise exceptions to notify format check failure. To do this, provide the decorator with expected Exception class or a tuple of expected classes. The downside of this approach is that the method has to always return some value, otherwise no exception will be still treated as a failure.
@format_checker.checks('even', AssertionError) def even_number(value): assert value % 2 == 0 return True
You can also register the format globally for all FormatChecker instances.
@FormatChecker.cls_checks('even') def even_number(value): return value % 2 == 0
Custom type in JSON schema
Now we will put all the previous example together to create an IValidator instance, that will have a custom validator is_positive and custom number format even. We will also add type float.
my_validator = MyValidator( schema, types={"float": float}, format_checker=format_checker )
Checking the result
To check that our new validator instance really accepts only positive, even float, let us add some unit test assertions.
# Positive but not even self.assertRaises(ValidationError, my_validator.validate, {'value': 1}) # Positive and even but not float self.assertRaises(ValidationError, my_validator.validate, {'value': 2}) # Positive and float but not even self.assertRaises(ValidationError, my_validator.validate, {'value': 3.0}) # Float and even but not positive self.assertRaises(ValidationError, my_validator.validate, {'value': -2.0}) # Even, but not positive nor float self.assertRaises(ValidationError, my_validator.validate, {'value': -2}) # Positive, float, and even self.assertIsNone(my_validator.validate({'value': 4.0}))
Complete example
#!/usr/bin/env python # -*- coding: utf-8 -*- from numbers import Number from unittest import TestCase, main from jsonschema import validators, Draft4Validator, FormatChecker from jsonschema.exceptions import ValidationError class TestCustomTypeValidatorFormat(TestCase): """Tests support for combination of custom type, validator and format.""" def test_combination(self): # Define JSON schema schema = { 'type': 'object', 'properties': { 'value': { 'type': 'float', 'format': 'even', 'is_positive': True }, } } # Define custom validators. Each must take exactly 4 arguments as below. def is_positive(validator, value, instance, schema): if not isinstance(instance, Number): yield ValidationError("%r is not a number" % instance) if value and instance <= 0: yield ValidationError("%r is not positive integer" % (instance,)) elif not value and instance > 0: yield ValidationError("%r is not negative integer nor zero" % (instance,)) # Add your custom validators among existing ones. all_validators = dict(Draft4Validator.VALIDATORS) all_validators['is_positive'] = is_positive # Create a new validator class. It will use your new validators and the schema # defined above. MyValidator = validators.create( meta_schema=Draft4Validator.META_SCHEMA, validators=all_validators ) # Create a new format checker instance. format_checker = FormatChecker() # Register a new format checker method for format 'even'. It must take exactly one # argument - the value for checking. @format_checker.checks('even') def even_number(value): return value % 2 == 0 # Create a new instance of your custom validator. Add a custom type. my_validator = MyValidator( schema, types={"float": float}, format_checker=format_checker ) # Now you can use your fully customized JSON schema validator. # Positive but not even self.assertRaises(ValidationError, my_validator.validate, {'value': 1}) # Positive and even but not float self.assertRaises(ValidationError, my_validator.validate, {'value': 2}) # Positive and float but not even self.assertRaises(ValidationError, my_validator.validate, {'value': 3.0}) # Float and even but not positive self.assertRaises(ValidationError, my_validator.validate, {'value': -2.0}) # Even, but not positive nor float self.assertRaises(ValidationError, my_validator.validate, {'value': -2}) # Positive, float, and even self.assertIsNone(my_validator.validate({'value': 4.0})) if __name__ == '__main__': main()
Leave a Reply