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 warnings import warn
13import contextlib
14import json
15import reprlib
16import warnings
17
18from attrs import define, field, fields
19from jsonschema_specifications import REGISTRY as SPECIFICATIONS
20from rpds import HashTrieMap
21import referencing.exceptions
22import referencing.jsonschema
23
24from jsonschema import (
25 _format,
26 _keywords,
27 _legacy_keywords,
28 _types,
29 _typing,
30 _utils,
31 exceptions,
32)
33
34if TYPE_CHECKING:
35 from jsonschema.protocols import Validator
36
37_UNSET = _utils.Unset()
38
39_VALIDATORS: dict[str, Validator] = {}
40_META_SCHEMAS = _utils.URIDict()
41
42
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}")
77
78
79def validates(version):
80 """
81 Register the decorated validator for a ``version`` of the specification.
82
83 Registered validators and their meta schemas will be considered when
84 parsing :kw:`$schema` keywords' URIs.
85
86 Arguments:
87
88 version (str):
89
90 An identifier to use as the version's name
91
92 Returns:
93
94 collections.abc.Callable:
95
96 a class decorator to decorate the validator with the version
97
98 """
99
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
106
107
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 )
129
130
131_REMOTE_WARNING_REGISTRY = SPECIFICATIONS.combine(
132 referencing.Registry(retrieve=_warn_for_remote_retrieve), # type: ignore[call-arg]
133)
134
135
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.
152
153 Arguments:
154
155 meta_schema:
156
157 the meta schema for the new validator class
158
159 validators:
160
161 a mapping from names to callables, where each callable will
162 validate the schema property with the given name.
163
164 Each callable should take 4 arguments:
165
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
171
172 version:
173
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.
179
180 type_checker:
181
182 a type checker, used when applying the :kw:`type` keyword.
183
184 If unprovided, a `jsonschema.TypeChecker` will be created
185 with a set of default types typical of JSON Schema drafts.
186
187 format_checker:
188
189 a format checker, used when applying the :kw:`format` keyword.
190
191 If unprovided, a `jsonschema.FormatChecker` will be created
192 with a set of default formats typical of JSON Schema drafts.
193
194 id_of:
195
196 A function that given a schema, returns its ID.
197
198 applicable_validators:
199
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.
208
209 Returns:
210
211 a new `jsonschema.protocols.Validator` class
212
213 """
214 # preemptively don't shadow the `Validator.format_checker` local
215 format_checker_arg = format_checker
216
217 specification = referencing.jsonschema.specification_with(
218 dialect_id=id_of(meta_schema) or "urn:unknown-dialect",
219 default=referencing.Specification.OPAQUE,
220 )
221
222 @define
223 class Validator:
224
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)
230
231 _APPLICABLE_VALIDATORS = applicable_validators
232 _validators = field(init=False, repr=False, eq=False)
233
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 )
249
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 )
264
265 def evolve(self, **changes):
266 cls = self.__class__
267 schema = changes.setdefault("schema", self.schema)
268 NewValidator = validator_for(schema, default=cls)
269
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)
277
278 return NewValidator(**changes)
279
280 cls.evolve = evolve
281
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)
289
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 ]
298
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)
305
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)
317
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
338
339 def evolve(self, **changes):
340 schema = changes.setdefault("schema", self.schema)
341 NewValidator = validator_for(schema, default=self.__class__)
342
343 for (attr_name, init_name) in evolve_fields:
344 if init_name not in changes:
345 changes[init_name] = getattr(self, attr_name)
346
347 return NewValidator(**changes)
348
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
368
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
380
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
395
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 )
414 return
415
416 if self._ref_resolver is not None:
417 evolved = self.evolve(schema=schema)
418 else:
419 if resolver is None:
420 resolver = self._resolver.in_subresource(
421 specification.create_resource(schema),
422 )
423 evolved = self.evolve(schema=schema, _resolver=resolver)
424
425 for k, v in applicable_validators(schema):
426 validator = evolved.VALIDATORS.get(k)
427 if validator is None:
428 continue
429
430 errors = validator(evolved, v, instance, schema) or ()
431 for error in errors:
432 # set details if not already set by the called fn
433 error._set(
434 validator=k,
435 validator_value=v,
436 instance=instance,
437 schema=schema,
438 type_checker=evolved.TYPE_CHECKER,
439 )
440 if k not in {"if", "$ref"}:
441 error.schema_path.appendleft(k)
442 if path is not None:
443 error.path.appendleft(path)
444 if schema_path is not None:
445 error.schema_path.appendleft(schema_path)
446 yield error
447
448 def validate(self, *args, **kwargs):
449 for error in self.iter_errors(*args, **kwargs):
450 raise error
451
452 def is_type(self, instance, type):
453 try:
454 return self.TYPE_CHECKER.is_type(instance, type)
455 except exceptions.UndefinedTypeCheck:
456 exc = exceptions.UnknownType(type, instance, self.schema)
457 raise exc from None
458
459 def _validate_reference(self, ref, instance):
460 if self._ref_resolver is None:
461 try:
462 resolved = self._resolver.lookup(ref)
463 except referencing.exceptions.Unresolvable as err:
464 raise exceptions._WrappedReferencingError(err) from err
465
466 return self.descend(
467 instance,
468 resolved.contents,
469 resolver=resolved.resolver,
470 )
471 else:
472 resolve = getattr(self._ref_resolver, "resolve", None)
473 if resolve is None:
474 with self._ref_resolver.resolving(ref) as resolved:
475 return self.descend(instance, resolved)
476 else:
477 scope, resolved = resolve(ref)
478 self._ref_resolver.push_scope(scope)
479
480 try:
481 return list(self.descend(instance, resolved))
482 finally:
483 self._ref_resolver.pop_scope()
484
485 def is_valid(self, instance, _schema=None):
486 if _schema is not None:
487 warnings.warn(
488 (
489 "Passing a schema to Validator.is_valid is deprecated "
490 "and will be removed in a future release. Call "
491 "validator.evolve(schema=new_schema).is_valid(...) "
492 "instead."
493 ),
494 DeprecationWarning,
495 stacklevel=2,
496 )
497 self = self.evolve(schema=_schema)
498
499 error = next(self.iter_errors(instance), None)
500 return error is None
501
502 evolve_fields = [
503 (field.name, field.alias)
504 for field in fields(Validator)
505 if field.init
506 ]
507
508 if version is not None:
509 safe = version.title().replace(" ", "").replace("-", "")
510 Validator.__name__ = Validator.__qualname__ = f"{safe}Validator"
511 Validator = validates(version)(Validator) # type: ignore[misc]
512
513 return Validator # type: ignore[return-value]
514
515
516def extend(
517 validator,
518 validators=(),
519 version=None,
520 type_checker=None,
521 format_checker=None,
522):
523 """
524 Create a new validator class by extending an existing one.
525
526 Arguments:
527
528 validator (jsonschema.protocols.Validator):
529
530 an existing validator class
531
532 validators (collections.abc.Mapping):
533
534 a mapping of new validator callables to extend with, whose
535 structure is as in `create`.
536
537 .. note::
538
539 Any validator callables with the same name as an
540 existing one will (silently) replace the old validator
541 callable entirely, effectively overriding any validation
542 done in the "parent" validator class.
543
544 If you wish to instead extend the behavior of a parent's
545 validator callable, delegate and call it directly in
546 the new validator function by retrieving it using
547 ``OldValidator.VALIDATORS["validation_keyword_name"]``.
548
549 version (str):
550
551 a version for the new validator class
552
553 type_checker (jsonschema.TypeChecker):
554
555 a type checker, used when applying the :kw:`type` keyword.
556
557 If unprovided, the type checker of the extended
558 `jsonschema.protocols.Validator` will be carried along.
559
560 format_checker (jsonschema.FormatChecker):
561
562 a format checker, used when applying the :kw:`format` keyword.
563
564 If unprovided, the format checker of the extended
565 `jsonschema.protocols.Validator` will be carried along.
566
567 Returns:
568
569 a new `jsonschema.protocols.Validator` class extending the one
570 provided
571
572 .. note:: Meta Schemas
573
574 The new validator class will have its parent's meta schema.
575
576 If you wish to change or extend the meta schema in the new
577 validator class, modify ``META_SCHEMA`` directly on the returned
578 class. Note that no implicit copying is done, so a copy should
579 likely be made before modifying it, in order to not affect the
580 old validator.
581
582 """
583 all_validators = dict(validator.VALIDATORS)
584 all_validators.update(validators)
585
586 if type_checker is None:
587 type_checker = validator.TYPE_CHECKER
588 if format_checker is None:
589 format_checker = validator.FORMAT_CHECKER
590 return create(
591 meta_schema=validator.META_SCHEMA,
592 validators=all_validators,
593 version=version,
594 type_checker=type_checker,
595 format_checker=format_checker,
596 id_of=validator.ID_OF,
597 applicable_validators=validator._APPLICABLE_VALIDATORS,
598 )
599
600
601Draft3Validator = create(
602 meta_schema=SPECIFICATIONS.contents(
603 "http://json-schema.org/draft-03/schema#",
604 ),
605 validators={
606 "$ref": _keywords.ref,
607 "additionalItems": _legacy_keywords.additionalItems,
608 "additionalProperties": _keywords.additionalProperties,
609 "dependencies": _legacy_keywords.dependencies_draft3,
610 "disallow": _legacy_keywords.disallow_draft3,
611 "divisibleBy": _keywords.multipleOf,
612 "enum": _keywords.enum,
613 "extends": _legacy_keywords.extends_draft3,
614 "format": _keywords.format,
615 "items": _legacy_keywords.items_draft3_draft4,
616 "maxItems": _keywords.maxItems,
617 "maxLength": _keywords.maxLength,
618 "maximum": _legacy_keywords.maximum_draft3_draft4,
619 "minItems": _keywords.minItems,
620 "minLength": _keywords.minLength,
621 "minimum": _legacy_keywords.minimum_draft3_draft4,
622 "pattern": _keywords.pattern,
623 "patternProperties": _keywords.patternProperties,
624 "properties": _legacy_keywords.properties_draft3,
625 "type": _legacy_keywords.type_draft3,
626 "uniqueItems": _keywords.uniqueItems,
627 },
628 type_checker=_types.draft3_type_checker,
629 format_checker=_format.draft3_format_checker,
630 version="draft3",
631 id_of=referencing.jsonschema.DRAFT3.id_of,
632 applicable_validators=_legacy_keywords.ignore_ref_siblings,
633)
634
635Draft4Validator = create(
636 meta_schema=SPECIFICATIONS.contents(
637 "http://json-schema.org/draft-04/schema#",
638 ),
639 validators={
640 "$ref": _keywords.ref,
641 "additionalItems": _legacy_keywords.additionalItems,
642 "additionalProperties": _keywords.additionalProperties,
643 "allOf": _keywords.allOf,
644 "anyOf": _keywords.anyOf,
645 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
646 "enum": _keywords.enum,
647 "format": _keywords.format,
648 "items": _legacy_keywords.items_draft3_draft4,
649 "maxItems": _keywords.maxItems,
650 "maxLength": _keywords.maxLength,
651 "maxProperties": _keywords.maxProperties,
652 "maximum": _legacy_keywords.maximum_draft3_draft4,
653 "minItems": _keywords.minItems,
654 "minLength": _keywords.minLength,
655 "minProperties": _keywords.minProperties,
656 "minimum": _legacy_keywords.minimum_draft3_draft4,
657 "multipleOf": _keywords.multipleOf,
658 "not": _keywords.not_,
659 "oneOf": _keywords.oneOf,
660 "pattern": _keywords.pattern,
661 "patternProperties": _keywords.patternProperties,
662 "properties": _keywords.properties,
663 "required": _keywords.required,
664 "type": _keywords.type,
665 "uniqueItems": _keywords.uniqueItems,
666 },
667 type_checker=_types.draft4_type_checker,
668 format_checker=_format.draft4_format_checker,
669 version="draft4",
670 id_of=referencing.jsonschema.DRAFT4.id_of,
671 applicable_validators=_legacy_keywords.ignore_ref_siblings,
672)
673
674Draft6Validator = create(
675 meta_schema=SPECIFICATIONS.contents(
676 "http://json-schema.org/draft-06/schema#",
677 ),
678 validators={
679 "$ref": _keywords.ref,
680 "additionalItems": _legacy_keywords.additionalItems,
681 "additionalProperties": _keywords.additionalProperties,
682 "allOf": _keywords.allOf,
683 "anyOf": _keywords.anyOf,
684 "const": _keywords.const,
685 "contains": _legacy_keywords.contains_draft6_draft7,
686 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
687 "enum": _keywords.enum,
688 "exclusiveMaximum": _keywords.exclusiveMaximum,
689 "exclusiveMinimum": _keywords.exclusiveMinimum,
690 "format": _keywords.format,
691 "items": _legacy_keywords.items_draft6_draft7_draft201909,
692 "maxItems": _keywords.maxItems,
693 "maxLength": _keywords.maxLength,
694 "maxProperties": _keywords.maxProperties,
695 "maximum": _keywords.maximum,
696 "minItems": _keywords.minItems,
697 "minLength": _keywords.minLength,
698 "minProperties": _keywords.minProperties,
699 "minimum": _keywords.minimum,
700 "multipleOf": _keywords.multipleOf,
701 "not": _keywords.not_,
702 "oneOf": _keywords.oneOf,
703 "pattern": _keywords.pattern,
704 "patternProperties": _keywords.patternProperties,
705 "properties": _keywords.properties,
706 "propertyNames": _keywords.propertyNames,
707 "required": _keywords.required,
708 "type": _keywords.type,
709 "uniqueItems": _keywords.uniqueItems,
710 },
711 type_checker=_types.draft6_type_checker,
712 format_checker=_format.draft6_format_checker,
713 version="draft6",
714 id_of=referencing.jsonschema.DRAFT6.id_of,
715 applicable_validators=_legacy_keywords.ignore_ref_siblings,
716)
717
718Draft7Validator = create(
719 meta_schema=SPECIFICATIONS.contents(
720 "http://json-schema.org/draft-07/schema#",
721 ),
722 validators={
723 "$ref": _keywords.ref,
724 "additionalItems": _legacy_keywords.additionalItems,
725 "additionalProperties": _keywords.additionalProperties,
726 "allOf": _keywords.allOf,
727 "anyOf": _keywords.anyOf,
728 "const": _keywords.const,
729 "contains": _legacy_keywords.contains_draft6_draft7,
730 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
731 "enum": _keywords.enum,
732 "exclusiveMaximum": _keywords.exclusiveMaximum,
733 "exclusiveMinimum": _keywords.exclusiveMinimum,
734 "format": _keywords.format,
735 "if": _keywords.if_,
736 "items": _legacy_keywords.items_draft6_draft7_draft201909,
737 "maxItems": _keywords.maxItems,
738 "maxLength": _keywords.maxLength,
739 "maxProperties": _keywords.maxProperties,
740 "maximum": _keywords.maximum,
741 "minItems": _keywords.minItems,
742 "minLength": _keywords.minLength,
743 "minProperties": _keywords.minProperties,
744 "minimum": _keywords.minimum,
745 "multipleOf": _keywords.multipleOf,
746 "not": _keywords.not_,
747 "oneOf": _keywords.oneOf,
748 "pattern": _keywords.pattern,
749 "patternProperties": _keywords.patternProperties,
750 "properties": _keywords.properties,
751 "propertyNames": _keywords.propertyNames,
752 "required": _keywords.required,
753 "type": _keywords.type,
754 "uniqueItems": _keywords.uniqueItems,
755 },
756 type_checker=_types.draft7_type_checker,
757 format_checker=_format.draft7_format_checker,
758 version="draft7",
759 id_of=referencing.jsonschema.DRAFT7.id_of,
760 applicable_validators=_legacy_keywords.ignore_ref_siblings,
761)
762
763Draft201909Validator = create(
764 meta_schema=SPECIFICATIONS.contents(
765 "https://json-schema.org/draft/2019-09/schema",
766 ),
767 validators={
768 "$recursiveRef": _legacy_keywords.recursiveRef,
769 "$ref": _keywords.ref,
770 "additionalItems": _legacy_keywords.additionalItems,
771 "additionalProperties": _keywords.additionalProperties,
772 "allOf": _keywords.allOf,
773 "anyOf": _keywords.anyOf,
774 "const": _keywords.const,
775 "contains": _keywords.contains,
776 "dependentRequired": _keywords.dependentRequired,
777 "dependentSchemas": _keywords.dependentSchemas,
778 "enum": _keywords.enum,
779 "exclusiveMaximum": _keywords.exclusiveMaximum,
780 "exclusiveMinimum": _keywords.exclusiveMinimum,
781 "format": _keywords.format,
782 "if": _keywords.if_,
783 "items": _legacy_keywords.items_draft6_draft7_draft201909,
784 "maxItems": _keywords.maxItems,
785 "maxLength": _keywords.maxLength,
786 "maxProperties": _keywords.maxProperties,
787 "maximum": _keywords.maximum,
788 "minItems": _keywords.minItems,
789 "minLength": _keywords.minLength,
790 "minProperties": _keywords.minProperties,
791 "minimum": _keywords.minimum,
792 "multipleOf": _keywords.multipleOf,
793 "not": _keywords.not_,
794 "oneOf": _keywords.oneOf,
795 "pattern": _keywords.pattern,
796 "patternProperties": _keywords.patternProperties,
797 "properties": _keywords.properties,
798 "propertyNames": _keywords.propertyNames,
799 "required": _keywords.required,
800 "type": _keywords.type,
801 "unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019,
802 "unevaluatedProperties": (
803 _legacy_keywords.unevaluatedProperties_draft2019
804 ),
805 "uniqueItems": _keywords.uniqueItems,
806 },
807 type_checker=_types.draft201909_type_checker,
808 format_checker=_format.draft201909_format_checker,
809 version="draft2019-09",
810)
811
812Draft202012Validator = create(
813 meta_schema=SPECIFICATIONS.contents(
814 "https://json-schema.org/draft/2020-12/schema",
815 ),
816 validators={
817 "$dynamicRef": _keywords.dynamicRef,
818 "$ref": _keywords.ref,
819 "additionalProperties": _keywords.additionalProperties,
820 "allOf": _keywords.allOf,
821 "anyOf": _keywords.anyOf,
822 "const": _keywords.const,
823 "contains": _keywords.contains,
824 "dependentRequired": _keywords.dependentRequired,
825 "dependentSchemas": _keywords.dependentSchemas,
826 "enum": _keywords.enum,
827 "exclusiveMaximum": _keywords.exclusiveMaximum,
828 "exclusiveMinimum": _keywords.exclusiveMinimum,
829 "format": _keywords.format,
830 "if": _keywords.if_,
831 "items": _keywords.items,
832 "maxItems": _keywords.maxItems,
833 "maxLength": _keywords.maxLength,
834 "maxProperties": _keywords.maxProperties,
835 "maximum": _keywords.maximum,
836 "minItems": _keywords.minItems,
837 "minLength": _keywords.minLength,
838 "minProperties": _keywords.minProperties,
839 "minimum": _keywords.minimum,
840 "multipleOf": _keywords.multipleOf,
841 "not": _keywords.not_,
842 "oneOf": _keywords.oneOf,
843 "pattern": _keywords.pattern,
844 "patternProperties": _keywords.patternProperties,
845 "prefixItems": _keywords.prefixItems,
846 "properties": _keywords.properties,
847 "propertyNames": _keywords.propertyNames,
848 "required": _keywords.required,
849 "type": _keywords.type,
850 "unevaluatedItems": _keywords.unevaluatedItems,
851 "unevaluatedProperties": _keywords.unevaluatedProperties,
852 "uniqueItems": _keywords.uniqueItems,
853 },
854 type_checker=_types.draft202012_type_checker,
855 format_checker=_format.draft202012_format_checker,
856 version="draft2020-12",
857)
858
859_LATEST_VERSION: type[Validator] = Draft202012Validator
860
861
862class _RefResolver:
863 """
864 Resolve JSON References.
865
866 Arguments:
867
868 base_uri (str):
869
870 The URI of the referring document
871
872 referrer:
873
874 The actual referring document
875
876 store (dict):
877
878 A mapping from URIs to documents to cache
879
880 cache_remote (bool):
881
882 Whether remote refs should be cached after first resolution
883
884 handlers (dict):
885
886 A mapping from URI schemes to functions that should be used
887 to retrieve them
888
889 urljoin_cache (:func:`functools.lru_cache`):
890
891 A cache that will be used for caching the results of joining
892 the resolution scope to subscopes.
893
894 remote_cache (:func:`functools.lru_cache`):
895
896 A cache that will be used for caching the results of
897 resolved remote URLs.
898
899 Attributes:
900
901 cache_remote (bool):
902
903 Whether remote refs should be cached after first resolution
904
905 .. deprecated:: v4.18.0
906
907 ``RefResolver`` has been deprecated in favor of `referencing`.
908
909 """
910
911 _DEPRECATION_MESSAGE = (
912 "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the "
913 "https://github.com/python-jsonschema/referencing library, which "
914 "provides more compliant referencing behavior as well as more "
915 "flexible APIs for customization. A future release will remove "
916 "RefResolver. Please file a feature request (on referencing) if you "
917 "are missing an API for the kind of customization you need."
918 )
919
920 def __init__(
921 self,
922 base_uri,
923 referrer,
924 store=HashTrieMap(),
925 cache_remote=True,
926 handlers=(),
927 urljoin_cache=None,
928 remote_cache=None,
929 ):
930 if urljoin_cache is None:
931 urljoin_cache = lru_cache(1024)(urljoin)
932 if remote_cache is None:
933 remote_cache = lru_cache(1024)(self.resolve_from_url)
934
935 self.referrer = referrer
936 self.cache_remote = cache_remote
937 self.handlers = dict(handlers)
938
939 self._scopes_stack = [base_uri]
940
941 self.store = _utils.URIDict(
942 (uri, each.contents) for uri, each in SPECIFICATIONS.items()
943 )
944 self.store.update(
945 (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items()
946 )
947 self.store.update(store)
948 self.store.update(
949 (schema["$id"], schema)
950 for schema in store.values()
951 if isinstance(schema, Mapping) and "$id" in schema
952 )
953 self.store[base_uri] = referrer
954
955 self._urljoin_cache = urljoin_cache
956 self._remote_cache = remote_cache
957
958 @classmethod
959 def from_schema( # noqa: D417
960 cls,
961 schema,
962 id_of=referencing.jsonschema.DRAFT202012.id_of,
963 *args,
964 **kwargs,
965 ):
966 """
967 Construct a resolver from a JSON schema object.
968
969 Arguments:
970
971 schema:
972
973 the referring schema
974
975 Returns:
976
977 `_RefResolver`
978
979 """
980 return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501
981
982 def push_scope(self, scope):
983 """
984 Enter a given sub-scope.
985
986 Treats further dereferences as being performed underneath the
987 given scope.
988 """
989 self._scopes_stack.append(
990 self._urljoin_cache(self.resolution_scope, scope),
991 )
992
993 def pop_scope(self):
994 """
995 Exit the most recent entered scope.
996
997 Treats further dereferences as being performed underneath the
998 original scope.
999
1000 Don't call this method more times than `push_scope` has been
1001 called.
1002 """
1003 try:
1004 self._scopes_stack.pop()
1005 except IndexError:
1006 raise exceptions._RefResolutionError(
1007 "Failed to pop the scope from an empty stack. "
1008 "`pop_scope()` should only be called once for every "
1009 "`push_scope()`",
1010 ) from None
1011
1012 @property
1013 def resolution_scope(self):
1014 """
1015 Retrieve the current resolution scope.
1016 """
1017 return self._scopes_stack[-1]
1018
1019 @property
1020 def base_uri(self):
1021 """
1022 Retrieve the current base URI, not including any fragment.
1023 """
1024 uri, _ = urldefrag(self.resolution_scope)
1025 return uri
1026
1027 @contextlib.contextmanager
1028 def in_scope(self, scope):
1029 """
1030 Temporarily enter the given scope for the duration of the context.
1031
1032 .. deprecated:: v4.0.0
1033 """
1034 warnings.warn(
1035 "jsonschema.RefResolver.in_scope is deprecated and will be "
1036 "removed in a future release.",
1037 DeprecationWarning,
1038 stacklevel=3,
1039 )
1040 self.push_scope(scope)
1041 try:
1042 yield
1043 finally:
1044 self.pop_scope()
1045
1046 @contextlib.contextmanager
1047 def resolving(self, ref):
1048 """
1049 Resolve the given ``ref`` and enter its resolution scope.
1050
1051 Exits the scope on exit of this context manager.
1052
1053 Arguments:
1054
1055 ref (str):
1056
1057 The reference to resolve
1058
1059 """
1060 url, resolved = self.resolve(ref)
1061 self.push_scope(url)
1062 try:
1063 yield resolved
1064 finally:
1065 self.pop_scope()
1066
1067 def _find_in_referrer(self, key):
1068 return self._get_subschemas_cache()[key]
1069
1070 @lru_cache # noqa: B019
1071 def _get_subschemas_cache(self):
1072 cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}
1073 for keyword, subschema in _search_schema(
1074 self.referrer, _match_subschema_keywords,
1075 ):
1076 cache[keyword].append(subschema)
1077 return cache
1078
1079 @lru_cache # noqa: B019
1080 def _find_in_subschemas(self, url):
1081 subschemas = self._get_subschemas_cache()["$id"]
1082 if not subschemas:
1083 return None
1084 uri, fragment = urldefrag(url)
1085 for subschema in subschemas:
1086 id = subschema["$id"]
1087 if not isinstance(id, str):
1088 continue
1089 target_uri = self._urljoin_cache(self.resolution_scope, id)
1090 if target_uri.rstrip("/") == uri.rstrip("/"):
1091 if fragment:
1092 subschema = self.resolve_fragment(subschema, fragment)
1093 self.store[url] = subschema
1094 return url, subschema
1095 return None
1096
1097 def resolve(self, ref):
1098 """
1099 Resolve the given reference.
1100 """
1101 url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/")
1102
1103 match = self._find_in_subschemas(url)
1104 if match is not None:
1105 return match
1106
1107 return url, self._remote_cache(url)
1108
1109 def resolve_from_url(self, url):
1110 """
1111 Resolve the given URL.
1112 """
1113 url, fragment = urldefrag(url)
1114 if not url:
1115 url = self.base_uri
1116
1117 try:
1118 document = self.store[url]
1119 except KeyError:
1120 try:
1121 document = self.resolve_remote(url)
1122 except Exception as exc:
1123 raise exceptions._RefResolutionError(exc) from exc
1124
1125 return self.resolve_fragment(document, fragment)
1126
1127 def resolve_fragment(self, document, fragment):
1128 """
1129 Resolve a ``fragment`` within the referenced ``document``.
1130
1131 Arguments:
1132
1133 document:
1134
1135 The referent document
1136
1137 fragment (str):
1138
1139 a URI fragment to resolve within it
1140
1141 """
1142 fragment = fragment.lstrip("/")
1143
1144 if not fragment:
1145 return document
1146
1147 if document is self.referrer:
1148 find = self._find_in_referrer
1149 else:
1150
1151 def find(key):
1152 yield from _search_schema(document, _match_keyword(key))
1153
1154 for keyword in ["$anchor", "$dynamicAnchor"]:
1155 for subschema in find(keyword):
1156 if fragment == subschema[keyword]:
1157 return subschema
1158 for keyword in ["id", "$id"]:
1159 for subschema in find(keyword):
1160 if "#" + fragment == subschema[keyword]:
1161 return subschema
1162
1163 # Resolve via path
1164 parts = unquote(fragment).split("/") if fragment else []
1165 for part in parts:
1166 part = part.replace("~1", "/").replace("~0", "~")
1167
1168 if isinstance(document, Sequence):
1169 try: # noqa: SIM105
1170 part = int(part)
1171 except ValueError:
1172 pass
1173 try:
1174 document = document[part]
1175 except (TypeError, LookupError) as err:
1176 raise exceptions._RefResolutionError(
1177 f"Unresolvable JSON pointer: {fragment!r}",
1178 ) from err
1179
1180 return document
1181
1182 def resolve_remote(self, uri):
1183 """
1184 Resolve a remote ``uri``.
1185
1186 If called directly, does not check the store first, but after
1187 retrieving the document at the specified URI it will be saved in
1188 the store if :attr:`cache_remote` is True.
1189
1190 .. note::
1191
1192 If the requests_ library is present, ``jsonschema`` will use it to
1193 request the remote ``uri``, so that the correct encoding is
1194 detected and used.
1195
1196 If it isn't, or if the scheme of the ``uri`` is not ``http`` or
1197 ``https``, UTF-8 is assumed.
1198
1199 Arguments:
1200
1201 uri (str):
1202
1203 The URI to resolve
1204
1205 Returns:
1206
1207 The retrieved document
1208
1209 .. _requests: https://pypi.org/project/requests/
1210
1211 """
1212 try:
1213 import requests
1214 except ImportError:
1215 requests = None
1216
1217 scheme = urlsplit(uri).scheme
1218
1219 if scheme in self.handlers:
1220 result = self.handlers[scheme](uri)
1221 elif scheme in ["http", "https"] and requests:
1222 # Requests has support for detecting the correct encoding of
1223 # json over http
1224 result = requests.get(uri).json()
1225 else:
1226 # Otherwise, pass off to urllib and assume utf-8
1227 from urllib.request import urlopen
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: type[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 # type: ignore[return-value]
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)