summaryrefslogtreecommitdiff
path: root/jsonschema-0.6/jsonschema.py
diff options
context:
space:
mode:
Diffstat (limited to 'jsonschema-0.6/jsonschema.py')
-rw-r--r--jsonschema-0.6/jsonschema.py658
1 files changed, 658 insertions, 0 deletions
diff --git a/jsonschema-0.6/jsonschema.py b/jsonschema-0.6/jsonschema.py
new file mode 100644
index 0000000..9910c84
--- /dev/null
+++ b/jsonschema-0.6/jsonschema.py
@@ -0,0 +1,658 @@
+"""
+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.6"
+
+
+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:
+ 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_dependencies(self, dependencies, instance, schema):
+ for property, dependency in iteritems(dependencies):
+ if property not in instance:
+ continue
+
+ if self.is_type(dependency, "object"):
+ for error in self.iter_errors(
+ instance, dependency, meta_validate=False
+ ):
+ yield error
+ else:
+ dependencies = _list(dependency)
+ for dependency in dependencies:
+ if dependency not in instance:
+ yield ValidationError(
+ "%r is a dependency of %r" % (dependency, property)
+ )
+
+ 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:
+ "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)