""" An implementation of JSON Schema for Python The main functionality is provided by the :class:`Validator` class, with the :function:`validate` function being the most common way to quickly create a :class:`Validator` object and validate an instance with a given schema. The :class:`Validator` class generally attempts to be as strict as possible under the JSON Schema specification. See its docstring for details. """ from __future__ import division, unicode_literals import collections import itertools import operator import re import sys import warnings PY3 = sys.version_info[0] >= 3 if PY3: basestring = unicode = str iteritems = operator.methodcaller("items") else: from itertools import izip as zip iteritems = operator.methodcaller("iteritems") def _uniq(container): """ Check if all of a container's elements are unique. Successively tries first to rely that the elements are hashable, then falls back on them being sortable, and finally falls back on brute force. """ try: return len(set(container)) == len(container) except TypeError: try: sort = sorted(container) sliced = itertools.islice(container, 1, None) for i, j in zip(container, sliced): if i == j: return False except (NotImplementedError, TypeError): seen = [] for e in container: if e in seen: return False seen.append(e) return True __version__ = "0.5" DRAFT_3 = { "$schema" : "http://json-schema.org/draft-03/schema#", "id" : "http://json-schema.org/draft-03/schema#", "type" : "object", "properties" : { "type" : { "type" : ["string", "array"], "items" : {"type" : ["string", {"$ref" : "#"}]}, "uniqueItems" : True, "default" : "any" }, "properties" : { "type" : "object", "additionalProperties" : {"$ref" : "#", "type": "object"}, "default" : {} }, "patternProperties" : { "type" : "object", "additionalProperties" : {"$ref" : "#"}, "default" : {} }, "additionalProperties" : { "type" : [{"$ref" : "#"}, "boolean"], "default" : {} }, "items" : { "type" : [{"$ref" : "#"}, "array"], "items" : {"$ref" : "#"}, "default" : {} }, "additionalItems" : { "type" : [{"$ref" : "#"}, "boolean"], "default" : {} }, "required" : {"type" : "boolean", "default" : False}, "dependencies" : { "type" : ["string", "array", "object"], "additionalProperties" : { "type" : ["string", "array", {"$ref" : "#"}], "items" : {"type" : "string"} }, "default" : {} }, "minimum" : {"type" : "number"}, "maximum" : {"type" : "number"}, "exclusiveMinimum" : {"type" : "boolean", "default" : False}, "exclusiveMaximum" : {"type" : "boolean", "default" : False}, "minItems" : {"type" : "integer", "minimum" : 0, "default" : 0}, "maxItems" : {"type" : "integer", "minimum" : 0}, "uniqueItems" : {"type" : "boolean", "default" : False}, "pattern" : {"type" : "string", "format" : "regex"}, "minLength" : {"type" : "integer", "minimum" : 0, "default" : 0}, "maxLength" : {"type" : "integer"}, "enum" : {"type" : "array", "minItems" : 1, "uniqueItems" : True}, "default" : {"type" : "any"}, "title" : {"type" : "string"}, "description" : {"type" : "string"}, "format" : {"type" : "string"}, "maxDecimal" : {"type" : "number", "minimum" : 0}, "divisibleBy" : { "type" : "number", "minimum" : 0, "exclusiveMinimum" : True, "default" : 1 }, "disallow" : { "type" : ["string", "array"], "items" : {"type" : ["string", {"$ref" : "#"}]}, "uniqueItems" : True }, "extends" : { "type" : [{"$ref" : "#"}, "array"], "items" : {"$ref" : "#"}, "default" : {} }, "id" : {"type" : "string", "format" : "uri"}, "$ref" : {"type" : "string", "format" : "uri"}, "$schema" : {"type" : "string", "format" : "uri"}, }, "dependencies" : { "exclusiveMinimum" : "minimum", "exclusiveMaximum" : "maximum" }, } EPSILON = 10 ** -15 class SchemaError(Exception): """ The provided schema is malformed. The same attributes exist for ``SchemaError``s as for ``ValidationError``s. """ validator = None def __init__(self, message): super(SchemaError, self).__init__(message) self.message = message self.path = [] class ValidationError(Exception): """ The instance didn't properly validate with the provided schema. Relevant attributes are: * ``message`` : a human readable message explaining the error * ``path`` : a list containing the path to the offending element (or [] if the error happened globally) in *reverse* order (i.e. deepest index first). """ # the failing validator will be set externally at whatever recursion level # is immediately above the validation failure validator = None def __init__(self, message): super(ValidationError, self).__init__(message) self.message = message # Any validator that recurses must append to the ValidationError's # path (e.g., properties and items) self.path = [] class Validator(object): """ A JSON Schema validator. """ DEFAULT_TYPES = { "array" : list, "boolean" : bool, "integer" : int, "null" : type(None), "number" : (int, float), "object" : dict, "string" : basestring, } def __init__( self, version=DRAFT_3, unknown_type="skip", unknown_property="skip", types=(), ): """ Initialize a Validator. ``version`` specifies which version of the JSON Schema specification to validate with. Currently only draft-03 is supported (and is the default). ``unknown_type`` and ``unknown_property`` control what to do when an unknown type (resp. property) is encountered. By default, the metaschema is respected (which e.g. for draft 3 allows a schema to have additional properties), but if for some reason you want to modify this behavior, you can do so without needing to modify the metaschema by passing ``"error"`` or ``"warn"`` to these arguments. ``types`` is a mapping (or iterable of 2-tuples) containing additional types or alternate types to verify via the 'type' property. For instance, the default types for the 'number' JSON Schema type are ``int`` and ``float``. To override this behavior (e.g. for also allowing ``decimal.Decimal``), pass ``types={"number" : (int, float, decimal.Decimal)} *including* the default types if so desired, which are fairly obvious but can be accessed via ``Validator.DEFAULT_TYPES`` if necessary. """ self._unknown_type = unknown_type self._unknown_property = unknown_property self._version = version self._types = dict(self.DEFAULT_TYPES) self._types.update(types) self._types["any"] = tuple(self._types.values()) def is_type(self, instance, type): """ Check if an ``instance`` is of the provided ``type``. """ py_type = self._types.get(type) if py_type is None: return self.schema_error( self._unknown_type, "%r is not a known type" % (type,) ) # the only thing we're careful about here is evading bool inheriting # from int, so let's be even dirtier than usual elif ( # it's not a bool, so no worries not isinstance(instance, bool) or # it is a bool, but we're checking for a bool, so no worries ( py_type is bool or isinstance(py_type, tuple) and bool in py_type ) ): return isinstance(instance, py_type) def schema_error(self, level, msg): if level == "skip": return elif level == "warn": warnings.warn(msg) else: raise SchemaError(msg) def is_valid(self, instance, schema, meta_validate=True): """ Check if the ``instance`` is valid under the ``schema``. Returns a bool indicating whether validation succeeded. """ error = next(self.iter_errors(instance, schema, meta_validate), None) return error is None def iter_errors(self, instance, schema, meta_validate=True): """ Lazily yield each of the errors in the given ``instance``. If you are unsure whether your schema itself is valid, ``meta_validate`` will first validate that the schema is valid before attempting to validate the instance. ``meta_validate`` is ``True`` by default, since setting it to ``False`` can lead to confusing error messages with an invalid schema. If you're sure your schema is in fact valid, or don't care, feel free to set this to ``False``. The meta validation will be done using the appropriate ``version``. """ if meta_validate: for error in self.iter_errors( schema, self._version, meta_validate=False ): s = SchemaError(error.message) s.path = error.path s.validator = error.validator # I think we're safer raising these always, not yielding them raise s for k, v in iteritems(schema): validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None) if validator is None: errors = self.unknown_property(k, instance, schema) else: errors = validator(v, instance, schema) for error in errors or (): # if the validator hasn't already been set (due to recursion) # make sure to set it error.validator = error.validator or k yield error def validate(self, *args, **kwargs): """ Validate an ``instance`` under the given ``schema``. """ for error in self.iter_errors(*args, **kwargs): raise error def unknown_property(self, property, instance, schema): self.schema_error( self._unknown_property, "%r is not a known schema property" % (property,) ) def validate_type(self, types, instance, schema): types = _list(types) for type in types: # Ouch. Brain hurts. Two paths here, either we have a schema, then # check if the instance is valid under it if (( self.is_type(type, "object") and self.is_valid(instance, type) # Or we have a type as a string, just check if the instance is that # type. Also, HACK: we can reach the `or` here if skip_types is # something other than error. If so, bail out. ) or ( self.is_type(type, "string") and (self.is_type(instance, type) or type not in self._types) )): return else: yield ValidationError( "%r is not of type %r" % (instance, _delist(types)) ) def validate_properties(self, properties, instance, schema): if not self.is_type(instance, "object"): return for property, subschema in iteritems(properties): if property in instance: dependencies = _list(subschema.get("dependencies", [])) if self.is_type(dependencies, "object"): for error in self.iter_errors( instance, dependencies, meta_validate=False ): yield error else: for dependency in dependencies: if dependency not in instance: yield ValidationError( "%r is a dependency of %r" % (dependency, property) ) for error in self.iter_errors( instance[property], subschema, meta_validate=False ): error.path.append(property) yield error elif subschema.get("required", False): error = ValidationError( "%r is a required property" % (property,) ) error.path.append(property) error.validator = "required" yield error def validate_patternProperties(self, patternProperties, instance, schema): for pattern, subschema in iteritems(patternProperties): for k, v in iteritems(instance): if re.match(pattern, k): for error in self.iter_errors( v, subschema, meta_validate=False ): yield error def validate_additionalProperties(self, aP, instance, schema): if not self.is_type(instance, "object"): return # no viewkeys in <2.7, and pypy seems to fail on vk - vk anyhow, so... extras = set(instance) - set(schema.get("properties", {})) if self.is_type(aP, "object"): for extra in extras: for error in self.iter_errors( instance[extra], aP, meta_validate=False ): yield error elif not aP and extras: error = "Additional properties are not allowed (%s %s unexpected)" yield ValidationError(error % _extras_msg(extras)) def validate_items(self, items, instance, schema): if not self.is_type(instance, "array"): return if self.is_type(items, "object"): for index, item in enumerate(instance): for error in self.iter_errors( item, items, meta_validate=False ): error.path.append(index) yield error else: for (index, item), subschema in zip(enumerate(instance), items): for error in self.iter_errors( item, subschema, meta_validate=False ): error.path.append(index) yield error def validate_additionalItems(self, aI, instance, schema): if not self.is_type(instance, "array"): return if self.is_type(aI, "object"): for item in instance[len(schema):]: for error in self.iter_errors(item, aI, meta_validate=False): yield error elif not aI and len(instance) > len(schema.get("items", [])): error = "Additional items are not allowed (%s %s unexpected)" yield ValidationError( error % _extras_msg(instance[len(schema) - 1:]) ) def validate_minimum(self, minimum, instance, schema): if not self.is_type(instance, "number"): return instance = float(instance) if schema.get("exclusiveMinimum", False): failed = instance <= minimum cmp = "less than or equal to" else: failed = instance < minimum cmp = "less than" if failed: yield ValidationError( "%r is %s the minimum of %r" % (instance, cmp, minimum) ) def validate_maximum(self, maximum, instance, schema): if not self.is_type(instance, "number"): return instance = float(instance) if schema.get("exclusiveMaximum", False): failed = instance >= maximum cmp = "greater than or equal to" else: failed = instance > maximum cmp = "greater than" if failed: yield ValidationError( "%r is %s the maximum of %r" % (instance, cmp, maximum) ) def validate_minItems(self, mI, instance, schema): if self.is_type(instance, "array") and len(instance) < mI: yield ValidationError("%r is too short" % (instance,)) def validate_maxItems(self, mI, instance, schema): if self.is_type(instance, "array") and len(instance) > mI: yield ValidationError("%r is too long" % (instance,)) def validate_uniqueItems(self, uI, instance, schema): if uI and self.is_type(instance, "array") and not _uniq(instance): yield ValidationError("%r has non-unique elements" % instance) def validate_pattern(self, patrn, instance, schema): if self.is_type(instance, "string") and not re.match(patrn, instance): yield ValidationError("%r does not match %r" % (instance, patrn)) def validate_minLength(self, mL, instance, schema): if self.is_type(instance, "string") and len(instance) < mL: yield ValidationError("%r is too short" % (instance,)) def validate_maxLength(self, mL, instance, schema): if self.is_type(instance, "string") and len(instance) > mL: yield ValidationError("%r is too long" % (instance,)) def validate_enum(self, enums, instance, schema): if instance not in enums: yield ValidationError("%r is not one of %r" % (instance, enums)) def validate_divisibleBy(self, dB, instance, schema): if not self.is_type(instance, "number"): return if isinstance(dB, float): mod = instance % dB failed = (mod > EPSILON) and (dB - mod) > EPSILON else: failed = instance % dB if failed: yield ValidationError("%r is not divisible by %r" % (instance, dB)) def validate_disallow(self, disallow, instance, schema): for disallowed in _list(disallow): if self.is_valid(instance, {"type" : [disallowed]}): yield ValidationError( "%r is disallowed for %r" % (disallowed, instance) ) def validate_extends(self, extends, instance, schema): if self.is_type(extends, "object"): extends = [extends] for subschema in extends: for error in self.iter_errors( instance, subschema, meta_validate=False ): yield error for no_op in [ # handled in: "dependencies", "required", # properties "exclusiveMinimum", "exclusiveMaximum", # min*/max* "default", "description", "format", "id", # no validation needed "links", "name", "title", "ref", "schema", # not yet supported ]: setattr(Validator, "validate_" + no_op, lambda *args, **kwargs : None) class ErrorTree(object): """ ErrorTrees make it easier to check which validations failed. """ def __init__(self, errors=()): self.errors = {} self._contents = collections.defaultdict(self.__class__) for error in errors: container = self for element in reversed(error.path): container = container[element] container.errors[error.validator] = error def __contains__(self, k): return k in self._contents def __getitem__(self, k): return self._contents[k] def __setitem__(self, k, v): self._contents[k] = v def __iter__(self): return iter(self._contents) def __len__(self): child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) return len(self.errors) + child_errors def __repr__(self): return "<%s (%s errors)>" % (self.__class__.__name__, len(self)) def _extras_msg(extras): """ Create an error message for extra items or properties. """ if len(extras) == 1: verb = "was" else: verb = "were" return ", ".join(repr(extra) for extra in extras), verb def _list(thing): """ Wrap ``thing`` in a list if it's a single str. Otherwise, return it unchanged. """ if isinstance(thing, basestring): return [thing] return thing def _delist(thing): """ Unwrap ``thing`` to a single element if its a single str in a list. Otherwise, return it unchanged. """ if ( isinstance(thing, list) and len(thing) == 1 and isinstance(thing[0], basestring) ): return thing[0] return thing def validate( instance, schema, meta_validate=True, cls=Validator, *args, **kwargs ): """ Validate an ``instance`` under the given ``schema``. By default, the :class:`Validator` class from this module is used to perform the validation. To use another validator, pass it into the ``cls`` argument. Any other provided positional and keyword arguments will be provided to the ``cls``. See the :class:`Validator` class' docstring for details on the arguments it accepts. """ validator = cls(*args, **kwargs) validator.validate(instance, schema, meta_validate=meta_validate)