Programming, Photography, Guides, Thoughts

jsonchema: Custom type, format and validator in Python


json­s­che­ma is a gre­at lib­ra­ry for vali­da­ting JSON data with JSON sche­ma in Python. It also allows you to spe­ci­fy cus­tom JSON sche­ma types, for­mats and even vali­da­tors. Unfor­tu­na­te­ly, the docu­men­tati­on is not very clear about the way how to cre­a­te cus­to­mi­zed validators.

This gui­de will show you an exam­ple how to cre­a­te a cus­tom vali­da­tor, that will add:

  • Type flo­at – json­s­che­ma knows by default only type num­ber, which covers all num­bers (inte­gers, flo­a­ting point num­bers, com­plex numbers, …).
  • For­mat even – limi­ting any num­ber to even num­bers only.
  • Vali­da­tor is_positive – accep­ting only posi­ti­ve num­bers when set to True and all the rest if set to Fal­se.

Custom jsonschema validator class

To add sup­port for cus­tom type, for­mat, and vali­da­tor, we must cre­a­te a new IVa­li­da­tor class. We will base it on Draft4Validator. Then we will cre­a­te a functi­on that will work as vali­da­tor for posi­ti­ve num­bers, cre­a­te ano­ther functi­on as a for­mat chec­ker for even num­bers and last­ly, add flo­at as a new type.

We will wri­te the exam­ple as a unit test Test­Ca­se, so we can easi­ly test at the end that our new vali­da­tor works as expected.

Schema definition

Let us first add requi­red imports and defi­ne the sche­ma that we will use for chec­king 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 sche­ma will accept JSON in for­mat {‘value’: num­ber}, whe­re num­ber is even, posi­ti­ve and flo­at (2.0, 4.0, etc.).

Custom JSON schema validator

Next, we will defi­ne a vali­da­tor, that will vali­da­te a (non-)positive num­ber based on its settings. The vali­da­tor can be set to a Boo­le­an value. Vali­da­tor must be a functi­on, that accepts exact­ly 4 argu­ments in this order:

  • vali­da­tor – instan­ce of vali­da­tor that is cal­ling this method.
  • value – value set to the vali­da­tor in the sche­ma (in our case True/Fal­se).
  • instan­ce – value that is being vali­da­ted (in our case the number).
  • sche­ma – the part of sche­ma whe­re this vali­da­tor 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 vali­da­tor must yield a Vali­dati­o­nError when the vali­dati­on fails. You can get inspi­red in jsonschema._validators. Let us add the new vali­da­tor among exis­ting ones and cre­a­te a new IVa­li­da­tor 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

Cre­a­ting a cus­tom for­mat chec­ker aga­in requi­res defi­ning a functi­on, that will check the for­mat. This method can be the regis­te­red either glo­bally for all For­matChec­ker instan­ces or tied to a sin­gle instan­ce. Let us defi­ne a for­mat chec­ker to check that a num­ber is even. First, we need a For­matChec­ker instan­ce. Then we will use this instan­ce as a deco­ra­tor for the for­mat chec­king functi­on. The functi­on must accept exact­ly one argu­ment – the value to check.

format_checker = FormatChecker()

@format_checker.checks('even')
def even_number(value):
    return value % 2 == 0

A for­mat chec­ker method can also rai­se excep­ti­ons to noti­fy for­mat check fai­lu­re. To do this, pro­vi­de the deco­ra­tor with expec­ted Excep­ti­on class or a tuple of expec­ted clas­ses. The down­si­de of this appro­ach is that the method has to alwa­ys return some value, otherwi­se no excep­ti­on will be still tre­a­ted as a failure.

@format_checker.checks('even', AssertionError)
def even_number(value):
    assert value % 2 == 0
    return True

You can also regis­ter the for­mat glo­bally for all For­matChec­ker instances.

@FormatChecker.cls_checks('even')
def even_number(value):
    return value % 2 == 0

Custom type in JSON schema

Now we will put all the pre­vi­ous exam­ple toge­ther to cre­a­te an IVa­li­da­tor instan­ce, that will have a cus­tom vali­da­tor is_positive and cus­tom num­ber for­mat even. We will also add type flo­at.

my_validator = MyValidator(
    schema, types={"float": float}, format_checker=format_checker
)

Checking the result

To check that our new vali­da­tor instan­ce really accepts only posi­ti­ve, even flo­at, 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()

Comments

5 responses to “jsonchema: Custom type, format and validator in Python”

  1. Tim Bouffard Avatar
    Tim Bouffard

    This was very hel­p­ful. The com­ple­te exam­ple works gre­at in ver­si­on 2.6.0 of json­s­che­ma. When try­ing the com­ple­te exam­ple on the newer, ear­ly rele­a­ses 3.0.0.a1,2 and 3 the exam­ple fails. The error is jsonschema.exceptions.UndefinedTypeCheck: Type ‘object’ is unk­nown to this type checker

    Even a pared down modi­fied ver­si­on of your exam­ple code has an issue. Not sure what I’m doing wrong.
    #!/usr/bin/env python
    # -*- coding: utf‑8 -*-

    import json­s­che­ma
    from json­s­che­ma import vali­da­te, Draft4Validator
    from jsonschema.exceptions import ValidationError

    def main():
    # Defi­ne JSON schema
    schema = {
    ‘type’: ‘object’,
    ‘pro­per­ties’: {
    ‘value’: {
    ‘type’: ‘num­ber’
    },
    }
    }

    all_validators = dict(Draft4Validator.VALIDATORS)

    MyVa­li­da­tor = jsonschema.validators.create(
    meta_schema=Draft4Validator.META_SCHEMA,
    validators=all_validators
    )

    print(‘my validator’)
    # This works in the new version
    try:
    print(validate({‘value’: ‘a’}, schema))
    except ValidationError:
    print(‘validation error when cal­ling validate’)

    # this does not work
    my_validator = MyValidator(schema)
    print(my_validator.is_valid({‘value’: ‘a’}))

    if __name__ == ‘__main__’:
    main()

  2. This is exact­ly what I nee­ded. Thank you!

  3. This is what we need! Thanks a lot.

  4. Aweso­me, exact­ly what I was loo­king for! Pity the­re are no exam­ples like these on the python-json­s­che­ma website.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.