Skip to content

Commit b1d7464

Browse files
committed
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.
1 parent eb026bd commit b1d7464

File tree

2 files changed

+173
-41
lines changed

2 files changed

+173
-41
lines changed

jsonschema.py

+92-41
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,42 @@ class SchemaError(Exception):
176176
"""
177177
The provided schema is malformed.
178178
179+
The same attributes exist for ``SchemaError``s as for ``ValidationError``s.
180+
179181
"""
180182

183+
validator = None
184+
185+
def __init__(self, message):
186+
super(SchemaError, self).__init__(message)
187+
self.message = message
188+
self.path = []
189+
190+
181191
class ValidationError(Exception):
182192
"""
183193
The instance didn't properly validate with the provided schema.
184194
195+
Relevant attributes are:
196+
* ``message`` : a human readable message explaining the error
197+
* ``path`` : a list containing the path to the offending element (or []
198+
if the error happened globally) in *reverse* order (i.e.
199+
deepest index first).
200+
185201
"""
186202

203+
# the failing validator will be set externally at whatever recursion level
204+
# is immediately above the validation failure
205+
validator = None
206+
207+
def __init__(self, message):
208+
super(ValidationError, self).__init__(message)
209+
self.message = message
210+
211+
# Any validator that recurses must append to the ValidationError's
212+
# path (e.g., properties and items)
213+
self.path = []
214+
187215

188216
class Validator(object):
189217
"""
@@ -350,11 +378,23 @@ def iter_errors(self, instance, schema):
350378

351379
try:
352380
if validator is None:
353-
self.unknown_property(k, instance, schema)
381+
errors = self.unknown_property(k, instance, schema)
354382
else:
355-
validator(v, instance, schema)
356-
except ValidationError as e:
357-
yield e
383+
errors = validator(v, instance, schema)
384+
except ValidationError as error:
385+
warnings.warn(
386+
"Raising errors from validators is deprecated. "
387+
"Please yield them instead.",
388+
DeprecationWarning,
389+
stacklevel=2
390+
)
391+
errors = [error]
392+
393+
for error in errors or ():
394+
# if the validator hasn't already been set (due to recursion)
395+
# make sure to set it
396+
error.validator = error.validator or k
397+
yield error
358398

359399
def _validate(self, instance, schema):
360400
warnings.warn(
@@ -417,7 +457,7 @@ def validate_type(self, types, instance, schema):
417457
)):
418458
return
419459
else:
420-
raise ValidationError(
460+
yield ValidationError(
421461
"%r is not of type %r" % (instance, _delist(types))
422462
)
423463

@@ -429,26 +469,31 @@ def validate_properties(self, properties, instance, schema):
429469
if property in instance:
430470
dependencies = _list(subschema.get("dependencies", []))
431471
if self.is_type(dependencies, "object"):
432-
self.validate(instance, dependencies)
472+
for error in self.iter_errors(instance, dependencies):
473+
yield error
433474
else:
434-
missing = (d for d in dependencies if d not in instance)
435-
first = next(missing, None)
436-
if first is not None:
437-
raise ValidationError(
438-
"%r is a dependency of %r" % (first, property)
439-
)
440-
441-
self.validate(instance[property], subschema)
475+
for dependency in dependencies:
476+
if dependency not in instance:
477+
yield ValidationError(
478+
"%r is a dependency of %r" % (dependency, property)
479+
)
480+
481+
for error in self.iter_errors(instance[property], subschema):
482+
error.path.append(property)
483+
yield error
442484
elif subschema.get("required", False):
443-
raise ValidationError(
485+
error = ValidationError(
444486
"%r is a required property" % (property,)
445487
)
488+
error.validator = "required"
489+
yield error
446490

447491
def validate_patternProperties(self, patternProperties, instance, schema):
448492
for pattern, subschema in iteritems(patternProperties):
449493
for k, v in iteritems(instance):
450494
if re.match(pattern, k):
451-
self.validate(v, subschema)
495+
for error in self.iter_errors(v, subschema):
496+
yield error
452497

453498
def validate_additionalProperties(self, aP, instance, schema):
454499
if not self.is_type(instance, "object"):
@@ -459,32 +504,38 @@ def validate_additionalProperties(self, aP, instance, schema):
459504

460505
if self.is_type(aP, "object"):
461506
for extra in extras:
462-
self.validate(instance[extra], aP)
507+
for error in self.iter_errors(instance[extra], aP):
508+
yield error
463509
elif not aP and extras:
464510
error = "Additional properties are not allowed (%s %s unexpected)"
465-
raise ValidationError(error % _extras_msg(extras))
511+
yield ValidationError(error % _extras_msg(extras))
466512

467513
def validate_items(self, items, instance, schema):
468514
if not self.is_type(instance, "array"):
469515
return
470516

471517
if self.is_type(items, "object"):
472-
for item in instance:
473-
self.validate(item, items)
518+
for index, item in enumerate(instance):
519+
for error in self.iter_errors(item, items):
520+
error.path.append(index)
521+
yield error
474522
else:
475-
for item, subschema in zip(instance, items):
476-
self.validate(item, subschema)
523+
for (index, item), subschema in zip(enumerate(instance), items):
524+
for error in self.iter_errors(item, subschema):
525+
error.path.append(index)
526+
yield error
477527

478528
def validate_additionalItems(self, aI, instance, schema):
479529
if not self.is_type(instance, "array"):
480530
return
481531

482532
if self.is_type(aI, "object"):
483533
for item in instance[len(schema):]:
484-
self.validate(item, aI)
534+
for error in self.iter_errors(item, aI):
535+
yield error
485536
elif not aI and len(instance) > len(schema.get("items", [])):
486537
error = "Additional items are not allowed (%s %s unexpected)"
487-
raise ValidationError(
538+
yield ValidationError(
488539
error % _extras_msg(instance[len(schema) - 1:])
489540
)
490541

@@ -501,7 +552,7 @@ def validate_minimum(self, minimum, instance, schema):
501552
cmp = "less than"
502553

503554
if failed:
504-
raise ValidationError(
555+
yield ValidationError(
505556
"%r is %s the minimum of %r" % (instance, cmp, minimum)
506557
)
507558

@@ -518,37 +569,37 @@ def validate_maximum(self, maximum, instance, schema):
518569
cmp = "greater than"
519570

520571
if failed:
521-
raise ValidationError(
572+
yield ValidationError(
522573
"%r is %s the maximum of %r" % (instance, cmp, maximum)
523574
)
524575

525576
def validate_minItems(self, mI, instance, schema):
526577
if self.is_type(instance, "array") and len(instance) < mI:
527-
raise ValidationError("%r is too short" % (instance,))
578+
yield ValidationError("%r is too short" % (instance,))
528579

529580
def validate_maxItems(self, mI, instance, schema):
530581
if self.is_type(instance, "array") and len(instance) > mI:
531-
raise ValidationError("%r is too long" % (instance,))
582+
yield ValidationError("%r is too long" % (instance,))
532583

533584
def validate_uniqueItems(self, uI, instance, schema):
534585
if uI and self.is_type(instance, "array") and not _uniq(instance):
535-
raise ValidationError("%r has non-unique elements" % instance)
586+
yield ValidationError("%r has non-unique elements" % instance)
536587

537588
def validate_pattern(self, patrn, instance, schema):
538589
if self.is_type(instance, "string") and not re.match(patrn, instance):
539-
raise ValidationError("%r does not match %r" % (instance, patrn))
590+
yield ValidationError("%r does not match %r" % (instance, patrn))
540591

541592
def validate_minLength(self, mL, instance, schema):
542593
if self.is_type(instance, "string") and len(instance) < mL:
543-
raise ValidationError("%r is too short" % (instance,))
594+
yield ValidationError("%r is too short" % (instance,))
544595

545596
def validate_maxLength(self, mL, instance, schema):
546597
if self.is_type(instance, "string") and len(instance) > mL:
547-
raise ValidationError("%r is too long" % (instance,))
598+
yield ValidationError("%r is too long" % (instance,))
548599

549600
def validate_enum(self, enums, instance, schema):
550601
if instance not in enums:
551-
raise ValidationError("%r is not one of %r" % (instance, enums))
602+
yield ValidationError("%r is not one of %r" % (instance, enums))
552603

553604
def validate_divisibleBy(self, dB, instance, schema):
554605
if not self.is_type(instance, "number"):
@@ -561,21 +612,21 @@ def validate_divisibleBy(self, dB, instance, schema):
561612
failed = instance % dB
562613

563614
if failed:
564-
raise ValidationError("%r is not divisible by %r" % (instance, dB))
615+
yield ValidationError("%r is not divisible by %r" % (instance, dB))
565616

566617
def validate_disallow(self, disallow, instance, schema):
567-
disallow = _list(disallow)
568-
569-
if any(self.is_valid(instance, {"type" : [d]}) for d in disallow):
570-
raise ValidationError(
571-
"%r is disallowed for %r" % (_delist(disallow), instance)
572-
)
618+
for disallowed in _list(disallow):
619+
if self.is_valid(instance, {"type" : [disallowed]}):
620+
yield ValidationError(
621+
"%r is disallowed for %r" % (disallowed, instance)
622+
)
573623

574624
def validate_extends(self, extends, instance, schema):
575625
if self.is_type(extends, "object"):
576626
extends = [extends]
577627
for subschema in extends:
578-
self.validate(instance, subschema)
628+
for error in self.iter_errors(instance, subschema):
629+
yield error
579630

580631

581632
for no_op in [ # handled in:

tests.py

+81
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,20 @@ def test_minItems_invalid_string(self):
632632
with self.assertRaises(SchemaError):
633633
validate([1], {"minItems" : "1"}) # needs to be an integer
634634

635+
def test_iter_errors_multiple_failures_one_validator(self):
636+
instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"}
637+
schema = {
638+
"properties" : {
639+
"foo" : {"type" : "string"},
640+
"bar" : {"minItems" : 2},
641+
"baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]},
642+
}
643+
}
644+
645+
errors = list(Validator().iter_errors(instance, schema))
646+
self.assertEqual(len(errors), 4)
647+
648+
635649
class TestDeprecations(unittest.TestCase):
636650
# XXX: RemoveMe in 0.5
637651
def test_number_types_deprecated(self):
@@ -663,6 +677,73 @@ def test_error_deprecated(self):
663677
self.assertEqual(len(w), 2)
664678

665679

680+
class TestValidationErrorDetails(unittest.TestCase):
681+
682+
def sorted_errors(self, errors):
683+
return sorted(errors, key=lambda e : [str(err) for err in e.path])
684+
685+
# TODO: These really need unit tests for each individual validator, rather
686+
# than just these higher level tests.
687+
def test_single_nesting(self):
688+
instance = {"foo" : 2, "bar" : [1], "baz" : 15, "quux" : "spam"}
689+
schema = {
690+
"properties" : {
691+
"foo" : {"type" : "string"},
692+
"bar" : {"minItems" : 2},
693+
"baz" : {"maximum" : 10, "enum" : [2, 4, 6, 8]},
694+
}
695+
}
696+
697+
errors = Validator().iter_errors(instance, schema)
698+
e1, e2, e3, e4 = self.sorted_errors(errors)
699+
700+
self.assertEqual(e1.path, ["bar"])
701+
self.assertEqual(e2.path, ["baz"])
702+
self.assertEqual(e3.path, ["baz"])
703+
self.assertEqual(e4.path, ["foo"])
704+
705+
self.assertEqual(e1.validator, "minItems")
706+
self.assertEqual(e2.validator, "enum")
707+
self.assertEqual(e3.validator, "maximum")
708+
self.assertEqual(e4.validator, "type")
709+
710+
def test_multiple_nesting(self):
711+
instance = [1, {"foo" : 2, "bar" : {"baz" : [1]}}, "quux"]
712+
schema = {
713+
"type" : "string",
714+
"items" : {
715+
"type" : ["string", "object"],
716+
"properties" : {
717+
"foo" : {"enum" : [1, 3]},
718+
"bar" : {
719+
"type" : "array",
720+
"properties" : {
721+
"bar" : {"required" : True},
722+
"baz" : {"minItems" : 2},
723+
}
724+
}
725+
}
726+
}
727+
}
728+
729+
errors = Validator().iter_errors(instance, schema)
730+
e1, e2, e3, e4, e5, e6 = self.sorted_errors(errors)
731+
732+
self.assertEqual(e1.path, [])
733+
self.assertEqual(e2.path, [0])
734+
self.assertEqual(e3.path, ["bar", 1])
735+
self.assertEqual(e4.path, ["bar", 1])
736+
self.assertEqual(e5.path, ["baz", "bar", 1])
737+
self.assertEqual(e6.path, ["foo", 1])
738+
739+
self.assertEqual(e1.validator, "type")
740+
self.assertEqual(e2.validator, "type")
741+
self.assertEqual(e3.validator, "type")
742+
self.assertEqual(e4.validator, "required")
743+
self.assertEqual(e5.validator, "minItems")
744+
self.assertEqual(e6.validator, "enum")
745+
746+
666747
class TestIgnorePropertiesForIrrelevantTypes(unittest.TestCase):
667748
def test_minimum(self):
668749
validate("x", {"type": ["string", "number"], "minimum": 10})

0 commit comments

Comments
 (0)