summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Berman <Julian+git@GrayVines.com>2012-06-18 14:52:12 -0400
committerJulian Berman <Julian+git@GrayVines.com>2012-06-18 14:52:12 -0400
commitb1d7464cddabaa8fab11113537328cd286768496 (patch)
treeaf61ce072dba4a3a28ce10781f3c7b50c20b5c60
parenteb026bd526132b113fec719ecf00532b15f9a38c (diff)
downloadjsonschema-b1d7464cddabaa8fab11113537328cd286768496.tar.gz
Provide error details for each ValidationError.
Closes #5. To make this work, raising errors from validators is now deprecated. Instead, each validator needs to yield each error it wishes to signal. This was probably broken before anyhow with stop_on_error but there wasn't a covering unit test at the time for it.
-rw-r--r--jsonschema.py133
-rw-r--r--tests.py81
2 files changed, 173 insertions, 41 deletions
diff --git a/jsonschema.py b/jsonschema.py
index ab48476..38bff4b 100644
--- a/jsonschema.py
+++ b/jsonschema.py
@@ -176,14 +176,42 @@ 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):
"""
@@ -350,11 +378,23 @@ class Validator(object):
try:
if validator is None:
- self.unknown_property(k, instance, schema)
+ errors = self.unknown_property(k, instance, schema)
else:
- validator(v, instance, schema)
- except ValidationError as e:
- yield e
+ errors = validator(v, instance, schema)
+ except ValidationError as error:
+ warnings.warn(
+ "Raising errors from validators is deprecated. "
+ "Please yield them instead.",
+ DeprecationWarning,
+ stacklevel=2
+ )
+ errors = [error]
+
+ 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, instance, schema):
warnings.warn(
@@ -417,7 +457,7 @@ class Validator(object):
)):
return
else:
- raise ValidationError(
+ yield ValidationError(
"%r is not of type %r" % (instance, _delist(types))
)
@@ -429,26 +469,31 @@ class Validator(object):
if property in instance:
dependencies = _list(subschema.get("dependencies", []))
if self.is_type(dependencies, "object"):
- self.validate(instance, dependencies)
+ for error in self.iter_errors(instance, dependencies):
+ yield error
else:
- missing = (d for d in dependencies if d not in instance)
- first = next(missing, None)
- if first is not None:
- raise ValidationError(
- "%r is a dependency of %r" % (first, property)
- )
-
- self.validate(instance[property], subschema)
+ 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):
+ error.path.append(property)
+ yield error
elif subschema.get("required", False):
- raise ValidationError(
+ error = ValidationError(
"%r is a required property" % (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):
- self.validate(v, subschema)
+ for error in self.iter_errors(v, subschema):
+ yield error
def validate_additionalProperties(self, aP, instance, schema):
if not self.is_type(instance, "object"):
@@ -459,21 +504,26 @@ class Validator(object):
if self.is_type(aP, "object"):
for extra in extras:
- self.validate(instance[extra], aP)
+ for error in self.iter_errors(instance[extra], aP):
+ yield error
elif not aP and extras:
error = "Additional properties are not allowed (%s %s unexpected)"
- raise ValidationError(error % _extras_msg(extras))
+ 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 item in instance:
- self.validate(item, items)
+ for index, item in enumerate(instance):
+ for error in self.iter_errors(item, items):
+ error.path.append(index)
+ yield error
else:
- for item, subschema in zip(instance, items):
- self.validate(item, subschema)
+ for (index, item), subschema in zip(enumerate(instance), items):
+ for error in self.iter_errors(item, subschema):
+ error.path.append(index)
+ yield error
def validate_additionalItems(self, aI, instance, schema):
if not self.is_type(instance, "array"):
@@ -481,10 +531,11 @@ class Validator(object):
if self.is_type(aI, "object"):
for item in instance[len(schema):]:
- self.validate(item, aI)
+ for error in self.iter_errors(item, aI):
+ yield error
elif not aI and len(instance) > len(schema.get("items", [])):
error = "Additional items are not allowed (%s %s unexpected)"
- raise ValidationError(
+ yield ValidationError(
error % _extras_msg(instance[len(schema) - 1:])
)
@@ -501,7 +552,7 @@ class Validator(object):
cmp = "less than"
if failed:
- raise ValidationError(
+ yield ValidationError(
"%r is %s the minimum of %r" % (instance, cmp, minimum)
)
@@ -518,37 +569,37 @@ class Validator(object):
cmp = "greater than"
if failed:
- raise ValidationError(
+ 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:
- raise ValidationError("%r is too short" % (instance,))
+ yield ValidationError("%r is too short" % (instance,))
def validate_maxItems(self, mI, instance, schema):
if self.is_type(instance, "array") and len(instance) > mI:
- raise ValidationError("%r is too long" % (instance,))
+ 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):
- raise ValidationError("%r has non-unique elements" % 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):
- raise ValidationError("%r does not match %r" % (instance, patrn))
+ 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:
- raise ValidationError("%r is too short" % (instance,))
+ yield ValidationError("%r is too short" % (instance,))
def validate_maxLength(self, mL, instance, schema):
if self.is_type(instance, "string") and len(instance) > mL:
- raise ValidationError("%r is too long" % (instance,))
+ yield ValidationError("%r is too long" % (instance,))
def validate_enum(self, enums, instance, schema):
if instance not in enums:
- raise ValidationError("%r is not one of %r" % (instance, 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"):
@@ -561,21 +612,21 @@ class Validator(object):
failed = instance % dB
if failed:
- raise ValidationError("%r is not divisible by %r" % (instance, dB))
+ yield ValidationError("%r is not divisible by %r" % (instance, dB))
def validate_disallow(self, disallow, instance, schema):
- disallow = _list(disallow)
-
- if any(self.is_valid(instance, {"type" : [d]}) for d in disallow):
- raise ValidationError(
- "%r is disallowed for %r" % (_delist(disallow), instance)
- )
+ 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:
- self.validate(instance, subschema)
+ for error in self.iter_errors(instance, subschema):
+ yield error
for no_op in [ # handled in:
diff --git a/tests.py b/tests.py
index 9a1f6ea..1a3e882 100644
--- a/tests.py
+++ b/tests.py
@@ -632,6 +632,20 @@ class TestValidate(ParameterizedTestCase, unittest.TestCase):
with self.assertRaises(SchemaError):
validate([1], {"minItems" : "1"}) # needs to be an integer
+ def test_iter_errors_multiple_failures_one_validator(self):
+ instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"}
+ schema = {
+ "properties" : {
+ "foo" : {"type" : "string"},
+ "bar" : {"minItems" : 2},
+ "baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]},
+ }
+ }
+
+ errors = list(Validator().iter_errors(instance, schema))
+ self.assertEqual(len(errors), 4)
+
+
class TestDeprecations(unittest.TestCase):
# XXX: RemoveMe in 0.5
def test_number_types_deprecated(self):
@@ -663,6 +677,73 @@ class TestDeprecations(unittest.TestCase):
self.assertEqual(len(w), 2)
+class TestValidationErrorDetails(unittest.TestCase):
+
+ def sorted_errors(self, errors):
+ return sorted(errors, key=lambda e : [str(err) for err in e.path])
+
+ # TODO: These really need unit tests for each individual validator, rather
+ # than just these higher level tests.
+ def test_single_nesting(self):
+ instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"}
+ schema = {
+ "properties" : {
+ "foo" : {"type" : "string"},
+ "bar" : {"minItems" : 2},
+ "baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]},
+ }
+ }
+
+ errors = Validator().iter_errors(instance, schema)
+ e1, e2, e3, e4 = self.sorted_errors(errors)
+
+ self.assertEqual(e1.path, ["bar"])
+ self.assertEqual(e2.path, ["baz"])
+ self.assertEqual(e3.path, ["baz"])
+ self.assertEqual(e4.path, ["foo"])
+
+ self.assertEqual(e1.validator, "minItems")
+ self.assertEqual(e2.validator, "enum")
+ self.assertEqual(e3.validator, "maximum")
+ self.assertEqual(e4.validator, "type")
+
+ def test_multiple_nesting(self):
+ instance = [1, {"foo" : 2, "bar" : {"baz" : [1]}}, "quux"]
+ schema = {
+ "type" : "string",
+ "items" : {
+ "type" : ["string", "object"],
+ "properties" : {
+ "foo" : {"enum" : [1, 3]},
+ "bar" : {
+ "type" : "array",
+ "properties" : {
+ "bar" : {"required" : True},
+ "baz" : {"minItems" : 2},
+ }
+ }
+ }
+ }
+ }
+
+ errors = Validator().iter_errors(instance, schema)
+ e1, e2, e3, e4, e5, e6 = self.sorted_errors(errors)
+
+ self.assertEqual(e1.path, [])
+ self.assertEqual(e2.path, [0])
+ self.assertEqual(e3.path, ["bar", 1])
+ self.assertEqual(e4.path, ["bar", 1])
+ self.assertEqual(e5.path, ["baz", "bar", 1])
+ self.assertEqual(e6.path, ["foo", 1])
+
+ self.assertEqual(e1.validator, "type")
+ self.assertEqual(e2.validator, "type")
+ self.assertEqual(e3.validator, "type")
+ self.assertEqual(e4.validator, "required")
+ self.assertEqual(e5.validator, "minItems")
+ self.assertEqual(e6.validator, "enum")
+
+
class TestIgnorePropertiesForIrrelevantTypes(unittest.TestCase):
def test_minimum(self):
validate("x", {"type": ["string", "number"], "minimum": 10})