jsonschema is a great library for validating JSON data with JSON schema in Python. It also allows you to specify custom JSON schema types, formats and even validators. Unfortunately, the documentation is not very clear about the way how to create customized validators.
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):
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
},
}
}
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 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,))
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,))
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
all_validators = dict(Draft4Validator.VALIDATORS)
all_validators['is_positive'] = is_positive
MyValidator = validators.create(
meta_schema=Draft4Validator.META_SCHEMA, validators=all_validators
)
all_validators = dict(Draft4Validator.VALIDATORS)
all_validators['is_positive'] = is_positive
MyValidator = validators.create(
meta_schema=Draft4Validator.META_SCHEMA, validators=all_validators
)
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')
format_checker = FormatChecker()
@format_checker.checks('even')
def even_number(value):
return value % 2 == 0
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)
@format_checker.checks('even', AssertionError)
def even_number(value):
assert value % 2 == 0
return True
@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')
@FormatChecker.cls_checks('even')
def even_number(value):
return value % 2 == 0
@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
my_validator = MyValidator(
schema, types={"float": float}, format_checker=format_checker
)
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.
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}))
# 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}))
# 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
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 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
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')
# 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.
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__':
#!/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()
#!/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