1"""
2Creation and extension of validators, with implementations for existing drafts.
3"""
4from __future__ import annotations
5
6from collections import deque
7from collections.abc import Iterable, Mapping, Sequence
8from functools import lru_cache
9from operator import methodcaller
10from typing import TYPE_CHECKING
11from urllib.parse import unquote, urldefrag, urljoin, urlsplit
12from urllib.request import urlopen
13from warnings import warn
14import contextlib
15import json
16import reprlib
17import warnings
18
19from attrs import define, field, fields
20from jsonschema_specifications import REGISTRY as SPECIFICATIONS
21from rpds import HashTrieMap
22import referencing.exceptions
23import referencing.jsonschema
24
25from jsonschema import (
26 _format,
27 _keywords,
28 _legacy_keywords,
29 _types,
30 _typing,
31 _utils,
32 exceptions,
33)
34
35if TYPE_CHECKING:
36 from jsonschema.protocols import Validator
37
38_UNSET = _utils.Unset()
39
40_VALIDATORS: dict[str, Validator] = {}
41_META_SCHEMAS = _utils.URIDict()
42
43
44def __getattr__(name):
45 if name == "ErrorTree":
46 warnings.warn(
47 "Importing ErrorTree from jsonschema.validators is deprecated. "
48 "Instead import it from jsonschema.exceptions.",
49 DeprecationWarning,
50 stacklevel=2,
51 )
52 from jsonschema.exceptions import ErrorTree
53 return ErrorTree
54 elif name == "validators":
55 warnings.warn(
56 "Accessing jsonschema.validators.validators is deprecated. "
57 "Use jsonschema.validators.validator_for with a given schema.",
58 DeprecationWarning,
59 stacklevel=2,
60 )
61 return _VALIDATORS
62 elif name == "meta_schemas":
63 warnings.warn(
64 "Accessing jsonschema.validators.meta_schemas is deprecated. "
65 "Use jsonschema.validators.validator_for with a given schema.",
66 DeprecationWarning,
67 stacklevel=2,
68 )
69 return _META_SCHEMAS
70 elif name == "RefResolver":
71 warnings.warn(
72 _RefResolver._DEPRECATION_MESSAGE,
73 DeprecationWarning,
74 stacklevel=2,
75 )
76 return _RefResolver
77 raise AttributeError(f"module {__name__} has no attribute {name}")
78
79
80def validates(version):
81 """
82 Register the decorated validator for a ``version`` of the specification.
83
84 Registered validators and their meta schemas will be considered when
85 parsing :kw:`$schema` keywords' URIs.
86
87 Arguments:
88
89 version (str):
90
91 An identifier to use as the version's name
92
93 Returns:
94
95 collections.abc.Callable:
96
97 a class decorator to decorate the validator with the version
98
99 """
100
101 def _validates(cls):
102 _VALIDATORS[version] = cls
103 meta_schema_id = cls.ID_OF(cls.META_SCHEMA)
104 _META_SCHEMAS[meta_schema_id] = cls
105 return cls
106 return _validates
107
108
109def _warn_for_remote_retrieve(uri: str):
110 from urllib.request import Request, urlopen
111 headers = {"User-Agent": "python-jsonschema (deprecated $ref resolution)"}
112 request = Request(uri, headers=headers) # noqa: S310
113 with urlopen(request) as response: # noqa: S310
114 warnings.warn(
115 "Automatically retrieving remote references can be a security "
116 "vulnerability and is discouraged by the JSON Schema "
117 "specifications. Relying on this behavior is deprecated "
118 "and will shortly become an error. If you are sure you want to "
119 "remotely retrieve your reference and that it is safe to do so, "
120 "you can find instructions for doing so via referencing.Registry "
121 "in the referencing documentation "
122 "(https://referencing.readthedocs.org).",
123 DeprecationWarning,
124 stacklevel=9, # Ha ha ha ha magic numbers :/
125 )
126 return referencing.Resource.from_contents(
127 json.load(response),
128 default_specification=referencing.jsonschema.DRAFT202012,
129 )
130
131
132_REMOTE_WARNING_REGISTRY = SPECIFICATIONS.combine(
133 referencing.Registry(retrieve=_warn_for_remote_retrieve), # type: ignore[call-arg]
134)
135
136
137def create(
138 meta_schema: referencing.jsonschema.ObjectSchema,
139 validators: (
140 Mapping[str, _typing.SchemaKeywordValidator]
141 | Iterable[tuple[str, _typing.SchemaKeywordValidator]]
142 ) = (),
143 version: str | None = None,
144 type_checker: _types.TypeChecker = _types.draft202012_type_checker,
145 format_checker: _format.FormatChecker = _format.draft202012_format_checker,
146 id_of: _typing.id_of = referencing.jsonschema.DRAFT202012.id_of,
147 applicable_validators: _typing.ApplicableValidators = methodcaller(
148 "items",
149 ),
150):
151 """
152 Create a new validator class.
153
154 Arguments:
155
156 meta_schema:
157
158 the meta schema for the new validator class
159
160 validators:
161
162 a mapping from names to callables, where each callable will
163 validate the schema property with the given name.
164
165 Each callable should take 4 arguments:
166
167 1. a validator instance,
168 2. the value of the property being validated within the
169 instance
170 3. the instance
171 4. the schema
172
173 version:
174
175 an identifier for the version that this validator class will
176 validate. If provided, the returned validator class will
177 have its ``__name__`` set to include the version, and also
178 will have `jsonschema.validators.validates` automatically
179 called for the given version.
180
181 type_checker:
182
183 a type checker, used when applying the :kw:`type` keyword.
184
185 If unprovided, a `jsonschema.TypeChecker` will be created
186 with a set of default types typical of JSON Schema drafts.
187
188 format_checker:
189
190 a format checker, used when applying the :kw:`format` keyword.
191
192 If unprovided, a `jsonschema.FormatChecker` will be created
193 with a set of default formats typical of JSON Schema drafts.
194
195 id_of:
196
197 A function that given a schema, returns its ID.
198
199 applicable_validators:
200
201 A function that, given a schema, returns the list of
202 applicable schema keywords and associated values
203 which will be used to validate the instance.
204 This is mostly used to support pre-draft 7 versions of JSON Schema
205 which specified behavior around ignoring keywords if they were
206 siblings of a ``$ref`` keyword. If you're not attempting to
207 implement similar behavior, you can typically ignore this argument
208 and leave it at its default.
209
210 Returns:
211
212 a new `jsonschema.protocols.Validator` class
213
214 """
215 # preemptively don't shadow the `Validator.format_checker` local
216 format_checker_arg = format_checker
217
218 specification = referencing.jsonschema.specification_with(
219 dialect_id=id_of(meta_schema) or "urn:unknown-dialect",
220 default=referencing.Specification.OPAQUE,
221 )
222
223 @define
224 class Validator:
225
226 VALIDATORS = dict(validators) # noqa: RUF012
227 META_SCHEMA = dict(meta_schema) # noqa: RUF012
228 TYPE_CHECKER = type_checker
229 FORMAT_CHECKER = format_checker_arg
230 ID_OF = staticmethod(id_of)
231
232 _APPLICABLE_VALIDATORS = applicable_validators
233 _validators = field(init=False, repr=False, eq=False)
234
235 schema: referencing.jsonschema.Schema = field(repr=reprlib.repr)
236 _ref_resolver = field(default=None, repr=False, alias="resolver")
237 format_checker: _format.FormatChecker | None = field(default=None)
238 # TODO: include new meta-schemas added at runtime
239 _registry: referencing.jsonschema.SchemaRegistry = field(
240 default=_REMOTE_WARNING_REGISTRY,
241 kw_only=True,
242 repr=False,
243 )
244 _resolver = field(
245 alias="_resolver",
246 default=None,
247 kw_only=True,
248 repr=False,
249 )
250
251 def __init_subclass__(cls):
252 warnings.warn(
253 (
254 "Subclassing validator classes is not intended to "
255 "be part of their public API. A future version "
256 "will make doing so an error, as the behavior of "
257 "subclasses isn't guaranteed to stay the same "
258 "between releases of jsonschema. Instead, prefer "
259 "composition of validators, wrapping them in an object "
260 "owned entirely by the downstream library."
261 ),
262 DeprecationWarning,
263 stacklevel=2,
264 )
265
266 def evolve(self, **changes):
267 cls = self.__class__
268 schema = changes.setdefault("schema", self.schema)
269 NewValidator = validator_for(schema, default=cls)
270
271 for field in fields(cls): # noqa: F402
272 if not field.init:
273 continue
274 attr_name = field.name
275 init_name = field.alias
276 if init_name not in changes:
277 changes[init_name] = getattr(self, attr_name)
278
279 return NewValidator(**changes)
280
281 cls.evolve = evolve
282
283 def __attrs_post_init__(self):
284 if self._resolver is None:
285 registry = self._registry
286 if registry is not _REMOTE_WARNING_REGISTRY:
287 registry = SPECIFICATIONS.combine(registry)
288 resource = specification.create_resource(self.schema)
289 self._resolver = registry.resolver_with_root(resource)
290
291 if self.schema is True or self.schema is False:
292 self._validators = []
293 else:
294 self._validators = [
295 (self.VALIDATORS[k], k, v)
296 for k, v in applicable_validators(self.schema)
297 if k in self.VALIDATORS
298 ]
299
300 # REMOVEME: Legacy ref resolution state management.
301 push_scope = getattr(self._ref_resolver, "push_scope", None)
302 if push_scope is not None:
303 id = id_of(self.schema)
304 if id is not None:
305 push_scope(id)
306
307 @classmethod
308 def check_schema(cls, schema, format_checker=_UNSET):
309 Validator = validator_for(cls.META_SCHEMA, default=cls)
310 if format_checker is _UNSET:
311 format_checker = Validator.FORMAT_CHECKER
312 validator = Validator(
313 schema=cls.META_SCHEMA,
314 format_checker=format_checker,
315 )
316 for error in validator.iter_errors(schema):
317 raise exceptions.SchemaError.create_from(error)
318
319 @property
320 def resolver(self):
321 warnings.warn(
322 (
323 f"Accessing {self.__class__.__name__}.resolver is "
324 "deprecated as of v4.18.0, in favor of the "
325 "https://github.com/python-jsonschema/referencing "
326 "library, which provides more compliant referencing "
327 "behavior as well as more flexible APIs for "
328 "customization."
329 ),
330 DeprecationWarning,
331 stacklevel=2,
332 )
333 if self._ref_resolver is None:
334 self._ref_resolver = _RefResolver.from_schema(
335 self.schema,
336 id_of=id_of,
337 )
338 return self._ref_resolver
339
340 def evolve(self, **changes):
341 schema = changes.setdefault("schema", self.schema)
342 NewValidator = validator_for(schema, default=self.__class__)
343
344 for (attr_name, init_name) in evolve_fields:
345 if init_name not in changes:
346 changes[init_name] = getattr(self, attr_name)
347
348 return NewValidator(**changes)
349
350 def iter_errors(self, instance, _schema=None):
351 if _schema is not None:
352 warnings.warn(
353 (
354 "Passing a schema to Validator.iter_errors "
355 "is deprecated and will be removed in a future "
356 "release. Call validator.evolve(schema=new_schema)."
357 "iter_errors(...) instead."
358 ),
359 DeprecationWarning,
360 stacklevel=2,
361 )
362 validators = [
363 (self.VALIDATORS[k], k, v)
364 for k, v in applicable_validators(_schema)
365 if k in self.VALIDATORS
366 ]
367 else:
368 _schema, validators = self.schema, self._validators
369
370 if _schema is True:
371 return
372 elif _schema is False:
373 yield exceptions.ValidationError(
374 f"False schema does not allow {instance!r}",
375 validator=None,
376 validator_value=None,
377 instance=instance,
378 schema=_schema,
379 )
380 return
381
382 for validator, k, v in validators:
383 errors = validator(self, v, instance, _schema) or ()
384 for error in errors:
385 # set details if not already set by the called fn
386 error._set(
387 validator=k,
388 validator_value=v,
389 instance=instance,
390 schema=_schema,
391 type_checker=self.TYPE_CHECKER,
392 )
393 if k not in {"if", "$ref"}:
394 error.schema_path.appendleft(k)
395 yield error
396
397 def descend(
398 self,
399 instance,
400 schema,
401 path=None,
402 schema_path=None,
403 resolver=None,
404 ):
405 if schema is True:
406 return
407 elif schema is False:
408 yield exceptions.ValidationError(
409 f"False schema does not allow {instance!r}",
410 validator=None,
411 validator_value=None,
412 instance=instance,
413 schema=schema,
414 )
415 return
416
417 if self._ref_resolver is not None:
418 evolved = self.evolve(schema=schema)
419 else:
420 if resolver is None:
421 resolver = self._resolver.in_subresource(
422 specification.create_resource(schema),
423 )
424 evolved = self.evolve(schema=schema, _resolver=resolver)
425
426 for k, v in applicable_validators(schema):
427 validator = evolved.VALIDATORS.get(k)
428 if validator is None:
429 continue
430
431 errors = validator(evolved, v, instance, schema) or ()
432 for error in errors:
433 # set details if not already set by the called fn
434 error._set(
435 validator=k,
436 validator_value=v,
437 instance=instance,
438 schema=schema,
439 type_checker=evolved.TYPE_CHECKER,
440 )
441 if k not in {"if", "$ref"}:
442 error.schema_path.appendleft(k)
443 if path is not None:
444 error.path.appendleft(path)
445 if schema_path is not None:
446 error.schema_path.appendleft(schema_path)
447 yield error
448
449 def validate(self, *args, **kwargs):
450 for error in self.iter_errors(*args, **kwargs):
451 raise error
452
453 def is_type(self, instance, type):
454 try:
455 return self.TYPE_CHECKER.is_type(instance, type)
456 except exceptions.UndefinedTypeCheck:
457 exc = exceptions.UnknownType(type, instance, self.schema)
458 raise exc from None
459
460 def _validate_reference(self, ref, instance):
461 if self._ref_resolver is None:
462 try:
463 resolved = self._resolver.lookup(ref)
464 except referencing.exceptions.Unresolvable as err:
465 raise exceptions._WrappedReferencingError(err) from err
466
467 return self.descend(
468 instance,
469 resolved.contents,
470 resolver=resolved.resolver,
471 )
472 else:
473 resolve = getattr(self._ref_resolver, "resolve", None)
474 if resolve is None:
475 with self._ref_resolver.resolving(ref) as resolved:
476 return self.descend(instance, resolved)
477 else:
478 scope, resolved = resolve(ref)
479 self._ref_resolver.push_scope(scope)
480
481 try:
482 return list(self.descend(instance, resolved))
483 finally:
484 self._ref_resolver.pop_scope()
485
486 def is_valid(self, instance, _schema=None):
487 if _schema is not None:
488 warnings.warn(
489 (
490 "Passing a schema to Validator.is_valid is deprecated "
491 "and will be removed in a future release. Call "
492 "validator.evolve(schema=new_schema).is_valid(...) "
493 "instead."
494 ),
495 DeprecationWarning,
496 stacklevel=2,
497 )
498 self = self.evolve(schema=_schema)
499
500 error = next(self.iter_errors(instance), None)
501 return error is None
502
503 evolve_fields = [
504 (field.name, field.alias)
505 for field in fields(Validator)
506 if field.init
507 ]
508
509 if version is not None:
510 safe = version.title().replace(" ", "").replace("-", "")
511 Validator.__name__ = Validator.__qualname__ = f"{safe}Validator"
512 Validator = validates(version)(Validator) # type: ignore[misc]
513
514 return Validator
515
516
517def extend(
518 validator,
519 validators=(),
520 version=None,
521 type_checker=None,
522 format_checker=None,
523):
524 """
525 Create a new validator class by extending an existing one.
526
527 Arguments:
528
529 validator (jsonschema.protocols.Validator):
530
531 an existing validator class
532
533 validators (collections.abc.Mapping):
534
535 a mapping of new validator callables to extend with, whose
536 structure is as in `create`.
537
538 .. note::
539
540 Any validator callables with the same name as an
541 existing one will (silently) replace the old validator
542 callable entirely, effectively overriding any validation
543 done in the "parent" validator class.
544
545 If you wish to instead extend the behavior of a parent's
546 validator callable, delegate and call it directly in
547 the new validator function by retrieving it using
548 ``OldValidator.VALIDATORS["validation_keyword_name"]``.
549
550 version (str):
551
552 a version for the new validator class
553
554 type_checker (jsonschema.TypeChecker):
555
556 a type checker, used when applying the :kw:`type` keyword.
557
558 If unprovided, the type checker of the extended
559 `jsonschema.protocols.Validator` will be carried along.
560
561 format_checker (jsonschema.FormatChecker):
562
563 a format checker, used when applying the :kw:`format` keyword.
564
565 If unprovided, the format checker of the extended
566 `jsonschema.protocols.Validator` will be carried along.
567
568 Returns:
569
570 a new `jsonschema.protocols.Validator` class extending the one
571 provided
572
573 .. note:: Meta Schemas
574
575 The new validator class will have its parent's meta schema.
576
577 If you wish to change or extend the meta schema in the new
578 validator class, modify ``META_SCHEMA`` directly on the returned
579 class. Note that no implicit copying is done, so a copy should
580 likely be made before modifying it, in order to not affect the
581 old validator.
582
583 """
584 all_validators = dict(validator.VALIDATORS)
585 all_validators.update(validators)
586
587 if type_checker is None:
588 type_checker = validator.TYPE_CHECKER
589 if format_checker is None:
590 format_checker = validator.FORMAT_CHECKER
591 return create(
592 meta_schema=validator.META_SCHEMA,
593 validators=all_validators,
594 version=version,
595 type_checker=type_checker,
596 format_checker=format_checker,
597 id_of=validator.ID_OF,
598 applicable_validators=validator._APPLICABLE_VALIDATORS,
599 )
600
601
602Draft3Validator = create(
603 meta_schema=SPECIFICATIONS.contents(
604 "http://json-schema.org/draft-03/schema#",
605 ),
606 validators={
607 "$ref": _keywords.ref,
608 "additionalItems": _legacy_keywords.additionalItems,
609 "additionalProperties": _keywords.additionalProperties,
610 "dependencies": _legacy_keywords.dependencies_draft3,
611 "disallow": _legacy_keywords.disallow_draft3,
612 "divisibleBy": _keywords.multipleOf,
613 "enum": _keywords.enum,
614 "extends": _legacy_keywords.extends_draft3,
615 "format": _keywords.format,
616 "items": _legacy_keywords.items_draft3_draft4,
617 "maxItems": _keywords.maxItems,
618 "maxLength": _keywords.maxLength,
619 "maximum": _legacy_keywords.maximum_draft3_draft4,
620 "minItems": _keywords.minItems,
621 "minLength": _keywords.minLength,
622 "minimum": _legacy_keywords.minimum_draft3_draft4,
623 "pattern": _keywords.pattern,
624 "patternProperties": _keywords.patternProperties,
625 "properties": _legacy_keywords.properties_draft3,
626 "type": _legacy_keywords.type_draft3,
627 "uniqueItems": _keywords.uniqueItems,
628 },
629 type_checker=_types.draft3_type_checker,
630 format_checker=_format.draft3_format_checker,
631 version="draft3",
632 id_of=referencing.jsonschema.DRAFT3.id_of,
633 applicable_validators=_legacy_keywords.ignore_ref_siblings,
634)
635
636Draft4Validator = create(
637 meta_schema=SPECIFICATIONS.contents(
638 "http://json-schema.org/draft-04/schema#",
639 ),
640 validators={
641 "$ref": _keywords.ref,
642 "additionalItems": _legacy_keywords.additionalItems,
643 "additionalProperties": _keywords.additionalProperties,
644 "allOf": _keywords.allOf,
645 "anyOf": _keywords.anyOf,
646 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
647 "enum": _keywords.enum,
648 "format": _keywords.format,
649 "items": _legacy_keywords.items_draft3_draft4,
650 "maxItems": _keywords.maxItems,
651 "maxLength": _keywords.maxLength,
652 "maxProperties": _keywords.maxProperties,
653 "maximum": _legacy_keywords.maximum_draft3_draft4,
654 "minItems": _keywords.minItems,
655 "minLength": _keywords.minLength,
656 "minProperties": _keywords.minProperties,
657 "minimum": _legacy_keywords.minimum_draft3_draft4,
658 "multipleOf": _keywords.multipleOf,
659 "not": _keywords.not_,
660 "oneOf": _keywords.oneOf,
661 "pattern": _keywords.pattern,
662 "patternProperties": _keywords.patternProperties,
663 "properties": _keywords.properties,
664 "required": _keywords.required,
665 "type": _keywords.type,
666 "uniqueItems": _keywords.uniqueItems,
667 },
668 type_checker=_types.draft4_type_checker,
669 format_checker=_format.draft4_format_checker,
670 version="draft4",
671 id_of=referencing.jsonschema.DRAFT4.id_of,
672 applicable_validators=_legacy_keywords.ignore_ref_siblings,
673)
674
675Draft6Validator = create(
676 meta_schema=SPECIFICATIONS.contents(
677 "http://json-schema.org/draft-06/schema#",
678 ),
679 validators={
680 "$ref": _keywords.ref,
681 "additionalItems": _legacy_keywords.additionalItems,
682 "additionalProperties": _keywords.additionalProperties,
683 "allOf": _keywords.allOf,
684 "anyOf": _keywords.anyOf,
685 "const": _keywords.const,
686 "contains": _legacy_keywords.contains_draft6_draft7,
687 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
688 "enum": _keywords.enum,
689 "exclusiveMaximum": _keywords.exclusiveMaximum,
690 "exclusiveMinimum": _keywords.exclusiveMinimum,
691 "format": _keywords.format,
692 "items": _legacy_keywords.items_draft6_draft7_draft201909,
693 "maxItems": _keywords.maxItems,
694 "maxLength": _keywords.maxLength,
695 "maxProperties": _keywords.maxProperties,
696 "maximum": _keywords.maximum,
697 "minItems": _keywords.minItems,
698 "minLength": _keywords.minLength,
699 "minProperties": _keywords.minProperties,
700 "minimum": _keywords.minimum,
701 "multipleOf": _keywords.multipleOf,
702 "not": _keywords.not_,
703 "oneOf": _keywords.oneOf,
704 "pattern": _keywords.pattern,
705 "patternProperties": _keywords.patternProperties,
706 "properties": _keywords.properties,
707 "propertyNames": _keywords.propertyNames,
708 "required": _keywords.required,
709 "type": _keywords.type,
710 "uniqueItems": _keywords.uniqueItems,
711 },
712 type_checker=_types.draft6_type_checker,
713 format_checker=_format.draft6_format_checker,
714 version="draft6",
715 id_of=referencing.jsonschema.DRAFT6.id_of,
716 applicable_validators=_legacy_keywords.ignore_ref_siblings,
717)
718
719Draft7Validator = create(
720 meta_schema=SPECIFICATIONS.contents(
721 "http://json-schema.org/draft-07/schema#",
722 ),
723 validators={
724 "$ref": _keywords.ref,
725 "additionalItems": _legacy_keywords.additionalItems,
726 "additionalProperties": _keywords.additionalProperties,
727 "allOf": _keywords.allOf,
728 "anyOf": _keywords.anyOf,
729 "const": _keywords.const,
730 "contains": _legacy_keywords.contains_draft6_draft7,
731 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
732 "enum": _keywords.enum,
733 "exclusiveMaximum": _keywords.exclusiveMaximum,
734 "exclusiveMinimum": _keywords.exclusiveMinimum,
735 "format": _keywords.format,
736 "if": _keywords.if_,
737 "items": _legacy_keywords.items_draft6_draft7_draft201909,
738 "maxItems": _keywords.maxItems,
739 "maxLength": _keywords.maxLength,
740 "maxProperties": _keywords.maxProperties,
741 "maximum": _keywords.maximum,
742 "minItems": _keywords.minItems,
743 "minLength": _keywords.minLength,
744 "minProperties": _keywords.minProperties,
745 "minimum": _keywords.minimum,
746 "multipleOf": _keywords.multipleOf,
747 "not": _keywords.not_,
748 "oneOf": _keywords.oneOf,
749 "pattern": _keywords.pattern,
750 "patternProperties": _keywords.patternProperties,
751 "properties": _keywords.properties,
752 "propertyNames": _keywords.propertyNames,
753 "required": _keywords.required,
754 "type": _keywords.type,
755 "uniqueItems": _keywords.uniqueItems,
756 },
757 type_checker=_types.draft7_type_checker,
758 format_checker=_format.draft7_format_checker,
759 version="draft7",
760 id_of=referencing.jsonschema.DRAFT7.id_of,
761 applicable_validators=_legacy_keywords.ignore_ref_siblings,
762)
763
764Draft201909Validator = create(
765 meta_schema=SPECIFICATIONS.contents(
766 "https://json-schema.org/draft/2019-09/schema",
767 ),
768 validators={
769 "$recursiveRef": _legacy_keywords.recursiveRef,
770 "$ref": _keywords.ref,
771 "additionalItems": _legacy_keywords.additionalItems,
772 "additionalProperties": _keywords.additionalProperties,
773 "allOf": _keywords.allOf,
774 "anyOf": _keywords.anyOf,
775 "const": _keywords.const,
776 "contains": _keywords.contains,
777 "dependentRequired": _keywords.dependentRequired,
778 "dependentSchemas": _keywords.dependentSchemas,
779 "enum": _keywords.enum,
780 "exclusiveMaximum": _keywords.exclusiveMaximum,
781 "exclusiveMinimum": _keywords.exclusiveMinimum,
782 "format": _keywords.format,
783 "if": _keywords.if_,
784 "items": _legacy_keywords.items_draft6_draft7_draft201909,
785 "maxItems": _keywords.maxItems,
786 "maxLength": _keywords.maxLength,
787 "maxProperties": _keywords.maxProperties,
788 "maximum": _keywords.maximum,
789 "minItems": _keywords.minItems,
790 "minLength": _keywords.minLength,
791 "minProperties": _keywords.minProperties,
792 "minimum": _keywords.minimum,
793 "multipleOf": _keywords.multipleOf,
794 "not": _keywords.not_,
795 "oneOf": _keywords.oneOf,
796 "pattern": _keywords.pattern,
797 "patternProperties": _keywords.patternProperties,
798 "properties": _keywords.properties,
799 "propertyNames": _keywords.propertyNames,
800 "required": _keywords.required,
801 "type": _keywords.type,
802 "unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019,
803 "unevaluatedProperties": (
804 _legacy_keywords.unevaluatedProperties_draft2019
805 ),
806 "uniqueItems": _keywords.uniqueItems,
807 },
808 type_checker=_types.draft201909_type_checker,
809 format_checker=_format.draft201909_format_checker,
810 version="draft2019-09",
811)
812
813Draft202012Validator = create(
814 meta_schema=SPECIFICATIONS.contents(
815 "https://json-schema.org/draft/2020-12/schema",
816 ),
817 validators={
818 "$dynamicRef": _keywords.dynamicRef,
819 "$ref": _keywords.ref,
820 "additionalProperties": _keywords.additionalProperties,
821 "allOf": _keywords.allOf,
822 "anyOf": _keywords.anyOf,
823 "const": _keywords.const,
824 "contains": _keywords.contains,
825 "dependentRequired": _keywords.dependentRequired,
826 "dependentSchemas": _keywords.dependentSchemas,
827 "enum": _keywords.enum,
828 "exclusiveMaximum": _keywords.exclusiveMaximum,
829 "exclusiveMinimum": _keywords.exclusiveMinimum,
830 "format": _keywords.format,
831 "if": _keywords.if_,
832 "items": _keywords.items,
833 "maxItems": _keywords.maxItems,
834 "maxLength": _keywords.maxLength,
835 "maxProperties": _keywords.maxProperties,
836 "maximum": _keywords.maximum,
837 "minItems": _keywords.minItems,
838 "minLength": _keywords.minLength,
839 "minProperties": _keywords.minProperties,
840 "minimum": _keywords.minimum,
841 "multipleOf": _keywords.multipleOf,
842 "not": _keywords.not_,
843 "oneOf": _keywords.oneOf,
844 "pattern": _keywords.pattern,
845 "patternProperties": _keywords.patternProperties,
846 "prefixItems": _keywords.prefixItems,
847 "properties": _keywords.properties,
848 "propertyNames": _keywords.propertyNames,
849 "required": _keywords.required,
850 "type": _keywords.type,
851 "unevaluatedItems": _keywords.unevaluatedItems,
852 "unevaluatedProperties": _keywords.unevaluatedProperties,
853 "uniqueItems": _keywords.uniqueItems,
854 },
855 type_checker=_types.draft202012_type_checker,
856 format_checker=_format.draft202012_format_checker,
857 version="draft2020-12",
858)
859
860_LATEST_VERSION = Draft202012Validator
861
862
863class _RefResolver:
864 """
865 Resolve JSON References.
866
867 Arguments:
868
869 base_uri (str):
870
871 The URI of the referring document
872
873 referrer:
874
875 The actual referring document
876
877 store (dict):
878
879 A mapping from URIs to documents to cache
880
881 cache_remote (bool):
882
883 Whether remote refs should be cached after first resolution
884
885 handlers (dict):
886
887 A mapping from URI schemes to functions that should be used
888 to retrieve them
889
890 urljoin_cache (:func:`functools.lru_cache`):
891
892 A cache that will be used for caching the results of joining
893 the resolution scope to subscopes.
894
895 remote_cache (:func:`functools.lru_cache`):
896
897 A cache that will be used for caching the results of
898 resolved remote URLs.
899
900 Attributes:
901
902 cache_remote (bool):
903
904 Whether remote refs should be cached after first resolution
905
906 .. deprecated:: v4.18.0
907
908 ``RefResolver`` has been deprecated in favor of `referencing`.
909
910 """
911
912 _DEPRECATION_MESSAGE = (
913 "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the "
914 "https://github.com/python-jsonschema/referencing library, which "
915 "provides more compliant referencing behavior as well as more "
916 "flexible APIs for customization. A future release will remove "
917 "RefResolver. Please file a feature request (on referencing) if you "
918 "are missing an API for the kind of customization you need."
919 )
920
921 def __init__(
922 self,
923 base_uri,
924 referrer,
925 store=HashTrieMap(),
926 cache_remote=True,
927 handlers=(),
928 urljoin_cache=None,
929 remote_cache=None,
930 ):
931 if urljoin_cache is None:
932 urljoin_cache = lru_cache(1024)(urljoin)
933 if remote_cache is None:
934 remote_cache = lru_cache(1024)(self.resolve_from_url)
935
936 self.referrer = referrer
937 self.cache_remote = cache_remote
938 self.handlers = dict(handlers)
939
940 self._scopes_stack = [base_uri]
941
942 self.store = _utils.URIDict(
943 (uri, each.contents) for uri, each in SPECIFICATIONS.items()
944 )
945 self.store.update(
946 (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items()
947 )
948 self.store.update(store)
949 self.store.update(
950 (schema["$id"], schema)
951 for schema in store.values()
952 if isinstance(schema, Mapping) and "$id" in schema
953 )
954 self.store[base_uri] = referrer
955
956 self._urljoin_cache = urljoin_cache
957 self._remote_cache = remote_cache
958
959 @classmethod
960 def from_schema( # noqa: D417
961 cls,
962 schema,
963 id_of=referencing.jsonschema.DRAFT202012.id_of,
964 *args,
965 **kwargs,
966 ):
967 """
968 Construct a resolver from a JSON schema object.
969
970 Arguments:
971
972 schema:
973
974 the referring schema
975
976 Returns:
977
978 `_RefResolver`
979
980 """
981 return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501
982
983 def push_scope(self, scope):
984 """
985 Enter a given sub-scope.
986
987 Treats further dereferences as being performed underneath the
988 given scope.
989 """
990 self._scopes_stack.append(
991 self._urljoin_cache(self.resolution_scope, scope),
992 )
993
994 def pop_scope(self):
995 """
996 Exit the most recent entered scope.
997
998 Treats further dereferences as being performed underneath the
999 original scope.
1000
1001 Don't call this method more times than `push_scope` has been
1002 called.
1003 """
1004 try:
1005 self._scopes_stack.pop()
1006 except IndexError:
1007 raise exceptions._RefResolutionError(
1008 "Failed to pop the scope from an empty stack. "
1009 "`pop_scope()` should only be called once for every "
1010 "`push_scope()`",
1011 ) from None
1012
1013 @property
1014 def resolution_scope(self):
1015 """
1016 Retrieve the current resolution scope.
1017 """
1018 return self._scopes_stack[-1]
1019
1020 @property
1021 def base_uri(self):
1022 """
1023 Retrieve the current base URI, not including any fragment.
1024 """
1025 uri, _ = urldefrag(self.resolution_scope)
1026 return uri
1027
1028 @contextlib.contextmanager
1029 def in_scope(self, scope):
1030 """
1031 Temporarily enter the given scope for the duration of the context.
1032
1033 .. deprecated:: v4.0.0
1034 """
1035 warnings.warn(
1036 "jsonschema.RefResolver.in_scope is deprecated and will be "
1037 "removed in a future release.",
1038 DeprecationWarning,
1039 stacklevel=3,
1040 )
1041 self.push_scope(scope)
1042 try:
1043 yield
1044 finally:
1045 self.pop_scope()
1046
1047 @contextlib.contextmanager
1048 def resolving(self, ref):
1049 """
1050 Resolve the given ``ref`` and enter its resolution scope.
1051
1052 Exits the scope on exit of this context manager.
1053
1054 Arguments:
1055
1056 ref (str):
1057
1058 The reference to resolve
1059
1060 """
1061 url, resolved = self.resolve(ref)
1062 self.push_scope(url)
1063 try:
1064 yield resolved
1065 finally:
1066 self.pop_scope()
1067
1068 def _find_in_referrer(self, key):
1069 return self._get_subschemas_cache()[key]
1070
1071 @lru_cache # noqa: B019
1072 def _get_subschemas_cache(self):
1073 cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}
1074 for keyword, subschema in _search_schema(
1075 self.referrer, _match_subschema_keywords,
1076 ):
1077 cache[keyword].append(subschema)
1078 return cache
1079
1080 @lru_cache # noqa: B019
1081 def _find_in_subschemas(self, url):
1082 subschemas = self._get_subschemas_cache()["$id"]
1083 if not subschemas:
1084 return None
1085 uri, fragment = urldefrag(url)
1086 for subschema in subschemas:
1087 id = subschema["$id"]
1088 if not isinstance(id, str):
1089 continue
1090 target_uri = self._urljoin_cache(self.resolution_scope, id)
1091 if target_uri.rstrip("/") == uri.rstrip("/"):
1092 if fragment:
1093 subschema = self.resolve_fragment(subschema, fragment)
1094 self.store[url] = subschema
1095 return url, subschema
1096 return None
1097
1098 def resolve(self, ref):
1099 """
1100 Resolve the given reference.
1101 """
1102 url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/")
1103
1104 match = self._find_in_subschemas(url)
1105 if match is not None:
1106 return match
1107
1108 return url, self._remote_cache(url)
1109
1110 def resolve_from_url(self, url):
1111 """
1112 Resolve the given URL.
1113 """
1114 url, fragment = urldefrag(url)
1115 if not url:
1116 url = self.base_uri
1117
1118 try:
1119 document = self.store[url]
1120 except KeyError:
1121 try:
1122 document = self.resolve_remote(url)
1123 except Exception as exc:
1124 raise exceptions._RefResolutionError(exc) from exc
1125
1126 return self.resolve_fragment(document, fragment)
1127
1128 def resolve_fragment(self, document, fragment):
1129 """
1130 Resolve a ``fragment`` within the referenced ``document``.
1131
1132 Arguments:
1133
1134 document:
1135
1136 The referent document
1137
1138 fragment (str):
1139
1140 a URI fragment to resolve within it
1141
1142 """
1143 fragment = fragment.lstrip("/")
1144
1145 if not fragment:
1146 return document
1147
1148 if document is self.referrer:
1149 find = self._find_in_referrer
1150 else:
1151
1152 def find(key):
1153 yield from _search_schema(document, _match_keyword(key))
1154
1155 for keyword in ["$anchor", "$dynamicAnchor"]:
1156 for subschema in find(keyword):
1157 if fragment == subschema[keyword]:
1158 return subschema
1159 for keyword in ["id", "$id"]:
1160 for subschema in find(keyword):
1161 if "#" + fragment == subschema[keyword]:
1162 return subschema
1163
1164 # Resolve via path
1165 parts = unquote(fragment).split("/") if fragment else []
1166 for part in parts:
1167 part = part.replace("~1", "/").replace("~0", "~")
1168
1169 if isinstance(document, Sequence):
1170 try: # noqa: SIM105
1171 part = int(part)
1172 except ValueError:
1173 pass
1174 try:
1175 document = document[part]
1176 except (TypeError, LookupError) as err:
1177 raise exceptions._RefResolutionError(
1178 f"Unresolvable JSON pointer: {fragment!r}",
1179 ) from err
1180
1181 return document
1182
1183 def resolve_remote(self, uri):
1184 """
1185 Resolve a remote ``uri``.
1186
1187 If called directly, does not check the store first, but after
1188 retrieving the document at the specified URI it will be saved in
1189 the store if :attr:`cache_remote` is True.
1190
1191 .. note::
1192
1193 If the requests_ library is present, ``jsonschema`` will use it to
1194 request the remote ``uri``, so that the correct encoding is
1195 detected and used.
1196
1197 If it isn't, or if the scheme of the ``uri`` is not ``http`` or
1198 ``https``, UTF-8 is assumed.
1199
1200 Arguments:
1201
1202 uri (str):
1203
1204 The URI to resolve
1205
1206 Returns:
1207
1208 The retrieved document
1209
1210 .. _requests: https://pypi.org/project/requests/
1211
1212 """
1213 try:
1214 import requests
1215 except ImportError:
1216 requests = None
1217
1218 scheme = urlsplit(uri).scheme
1219
1220 if scheme in self.handlers:
1221 result = self.handlers[scheme](uri)
1222 elif scheme in ["http", "https"] and requests:
1223 # Requests has support for detecting the correct encoding of
1224 # json over http
1225 result = requests.get(uri).json()
1226 else:
1227 # Otherwise, pass off to urllib and assume utf-8
1228 with urlopen(uri) as url: # noqa: S310
1229 result = json.loads(url.read().decode("utf-8"))
1230
1231 if self.cache_remote:
1232 self.store[uri] = result
1233 return result
1234
1235
1236_SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor")
1237
1238
1239def _match_keyword(keyword):
1240
1241 def matcher(value):
1242 if keyword in value:
1243 yield value
1244
1245 return matcher
1246
1247
1248def _match_subschema_keywords(value):
1249 for keyword in _SUBSCHEMAS_KEYWORDS:
1250 if keyword in value:
1251 yield keyword, value
1252
1253
1254def _search_schema(schema, matcher):
1255 """Breadth-first search routine."""
1256 values = deque([schema])
1257 while values:
1258 value = values.pop()
1259 if not isinstance(value, dict):
1260 continue
1261 yield from matcher(value)
1262 values.extendleft(value.values())
1263
1264
1265def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417
1266 """
1267 Validate an instance under the given schema.
1268
1269 >>> validate([2, 3, 4], {"maxItems": 2})
1270 Traceback (most recent call last):
1271 ...
1272 ValidationError: [2, 3, 4] is too long
1273
1274 :func:`~jsonschema.validators.validate` will first verify that the
1275 provided schema is itself valid, since not doing so can lead to less
1276 obvious error messages and fail in less obvious or consistent ways.
1277
1278 If you know you have a valid schema already, especially
1279 if you intend to validate multiple instances with
1280 the same schema, you likely would prefer using the
1281 `jsonschema.protocols.Validator.validate` method directly on a
1282 specific validator (e.g. ``Draft202012Validator.validate``).
1283
1284
1285 Arguments:
1286
1287 instance:
1288
1289 The instance to validate
1290
1291 schema:
1292
1293 The schema to validate with
1294
1295 cls (jsonschema.protocols.Validator):
1296
1297 The class that will be used to validate the instance.
1298
1299 If the ``cls`` argument is not provided, two things will happen
1300 in accordance with the specification. First, if the schema has a
1301 :kw:`$schema` keyword containing a known meta-schema [#]_ then the
1302 proper validator will be used. The specification recommends that
1303 all schemas contain :kw:`$schema` properties for this reason. If no
1304 :kw:`$schema` property is found, the default validator class is the
1305 latest released draft.
1306
1307 Any other provided positional and keyword arguments will be passed
1308 on when instantiating the ``cls``.
1309
1310 Raises:
1311
1312 `jsonschema.exceptions.ValidationError`:
1313
1314 if the instance is invalid
1315
1316 `jsonschema.exceptions.SchemaError`:
1317
1318 if the schema itself is invalid
1319
1320 .. rubric:: Footnotes
1321 .. [#] known by a validator registered with
1322 `jsonschema.validators.validates`
1323
1324 """
1325 if cls is None:
1326 cls = validator_for(schema)
1327
1328 cls.check_schema(schema)
1329 validator = cls(schema, *args, **kwargs)
1330 error = exceptions.best_match(validator.iter_errors(instance))
1331 if error is not None:
1332 raise error
1333
1334
1335def validator_for(
1336 schema,
1337 default: Validator | _utils.Unset = _UNSET,
1338) -> type[Validator]:
1339 """
1340 Retrieve the validator class appropriate for validating the given schema.
1341
1342 Uses the :kw:`$schema` keyword that should be present in the given
1343 schema to look up the appropriate validator class.
1344
1345 Arguments:
1346
1347 schema (collections.abc.Mapping or bool):
1348
1349 the schema to look at
1350
1351 default:
1352
1353 the default to return if the appropriate validator class
1354 cannot be determined.
1355
1356 If unprovided, the default is to return the latest supported
1357 draft.
1358
1359 Examples:
1360
1361 The :kw:`$schema` JSON Schema keyword will control which validator
1362 class is returned:
1363
1364 >>> schema = {
1365 ... "$schema": "https://json-schema.org/draft/2020-12/schema",
1366 ... "type": "integer",
1367 ... }
1368 >>> jsonschema.validators.validator_for(schema)
1369 <class 'jsonschema.validators.Draft202012Validator'>
1370
1371
1372 Here, a draft 7 schema instead will return the draft 7 validator:
1373
1374 >>> schema = {
1375 ... "$schema": "http://json-schema.org/draft-07/schema#",
1376 ... "type": "integer",
1377 ... }
1378 >>> jsonschema.validators.validator_for(schema)
1379 <class 'jsonschema.validators.Draft7Validator'>
1380
1381
1382 Schemas with no ``$schema`` keyword will fallback to the default
1383 argument:
1384
1385 >>> schema = {"type": "integer"}
1386 >>> jsonschema.validators.validator_for(
1387 ... schema, default=Draft7Validator,
1388 ... )
1389 <class 'jsonschema.validators.Draft7Validator'>
1390
1391 or if none is provided, to the latest version supported.
1392 Always including the keyword when authoring schemas is highly
1393 recommended.
1394
1395 """
1396 DefaultValidator = _LATEST_VERSION if default is _UNSET else default
1397
1398 if schema is True or schema is False or "$schema" not in schema:
1399 return DefaultValidator
1400 if schema["$schema"] not in _META_SCHEMAS and default is _UNSET:
1401 warn(
1402 (
1403 "The metaschema specified by $schema was not found. "
1404 "Using the latest draft to validate, but this will raise "
1405 "an error in the future."
1406 ),
1407 DeprecationWarning,
1408 stacklevel=2,
1409 )
1410 return _META_SCHEMAS.get(schema["$schema"], DefaultValidator)