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