summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Berman <Julian@GrayVines.com>2021-08-04 10:58:09 +0100
committerJulian Berman <Julian@GrayVines.com>2021-08-04 10:58:09 +0100
commit1a7a935fd3e4dc11089b0a9f6ac247d66dc6b358 (patch)
tree9ab27da280b462ac59510a715658a687c125a771
parent72a0c608f030a70c431dd1632e13d04e7370edff (diff)
parent4547b2ab8a74dd8a83c9104606b2166a9e712fa2 (diff)
downloadjsonschema-1a7a935fd3e4dc11089b0a9f6ac247d66dc6b358.tar.gz
Merge remote-tracking branch 'anexia-it/draft2020-12'
* anexia-it/draft2020-12: Julian/jsonschema#782: Remove ecmascript validation, extend dynamicRef skip description Julian/jsonschema#782: Add compatibility to draft7 and older Julian/jsonschema#782: Code clenaup, fixes validation messages Julian/jsonschema#782: Extend format tests Julian/jsonschema#782: Resolve meta schema vocabularies from local cache Julian/jsonschema#782: Refactor items behavior with prefixItems Julian/jsonschema#782: Update validation message for unevaluatedProperties and unevaluatedItems Julian/jsonschema#782: Fixes failing styles Julian/jsonschema#782: Adapt validator test for draft2020-12, fixes code styles Julian/jsonschema#782: Fixes relative json pointer format validation for leading zero on digit Julian/jsonschema#782: Load dependencies from legacy validators Julian/jsonschema#782: Implements dynamicRef validations Julian/jsonschema#782: Add validation for uuid format Julian/jsonschema#782: Implements defs validations Julian/jsonschema#782: Extend resolver for anchor Julian/jsonschema#782: Fixes ref resolver for folders Julian/jsonschema#782: Fixes ref validation priority Julian/jsonschema#782: Extend implementation of ref Julian/jsonschema#782: Implements unevaluatedProperties validations Julian/jsonschema#782: Implements unevaluatedItems validations Julian/jsonschema#782: Extend contains with minContains and maxContaints, add contains legacy validator Julian/jsonschema#782: Adapt items to work with prefixItems Julian/jsonschema#782: Add checks for prefixItems, basic check for unevaluatedItems Julian/jsonschema#782: Extend format check for draft2020-12, add duration format check Julian/jsonschema#782: Add dependentRequired and dependentSchemas validation Julian/jsonschema#782: Split format and regular test cases on draft2020-12 Julian/jsonschema#782: Enable draft2020-12 test suite
-rw-r--r--jsonschema/__init__.py2
-rw-r--r--jsonschema/_format.py84
-rw-r--r--jsonschema/_legacy_validators.py69
-rw-r--r--jsonschema/_types.py1
-rw-r--r--jsonschema/_utils.py158
-rw-r--r--jsonschema/_validators.py178
-rw-r--r--jsonschema/schemas/draft2020-12.json58
-rw-r--r--jsonschema/schemas/draft2020-12/applicator.json48
-rw-r--r--jsonschema/schemas/draft2020-12/content.json17
-rw-r--r--jsonschema/schemas/draft2020-12/core.json51
-rw-r--r--jsonschema/schemas/draft2020-12/format-annotation.json14
-rw-r--r--jsonschema/schemas/draft2020-12/meta-data.json37
-rw-r--r--jsonschema/schemas/draft2020-12/unevaluated.json15
-rw-r--r--jsonschema/schemas/draft2020-12/validation.json98
-rw-r--r--jsonschema/tests/test_cli.py6
-rw-r--r--jsonschema/tests/test_format.py25
-rw-r--r--jsonschema/tests/test_jsonschema_test_suite.py63
-rw-r--r--jsonschema/tests/test_validators.py72
-rw-r--r--jsonschema/validators.py154
-rw-r--r--setup.cfg4
20 files changed, 1095 insertions, 59 deletions
diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py
index 619a7ea..c1cb2e9 100644
--- a/jsonschema/__init__.py
+++ b/jsonschema/__init__.py
@@ -14,6 +14,7 @@ from jsonschema._format import (
draft4_format_checker,
draft6_format_checker,
draft7_format_checker,
+ draft202012_format_checker,
)
from jsonschema._types import TypeChecker
from jsonschema.exceptions import (
@@ -28,6 +29,7 @@ from jsonschema.validators import (
Draft4Validator,
Draft6Validator,
Draft7Validator,
+ Draft202012Validator,
RefResolver,
validate,
)
diff --git a/jsonschema/_format.py b/jsonschema/_format.py
index bdd893d..19f4283 100644
--- a/jsonschema/_format.py
+++ b/jsonschema/_format.py
@@ -1,3 +1,4 @@
+from uuid import UUID
import datetime
import ipaddress
import re
@@ -131,13 +132,14 @@ draft3_format_checker = FormatChecker()
draft4_format_checker = FormatChecker()
draft6_format_checker = FormatChecker()
draft7_format_checker = FormatChecker()
-
+draft202012_format_checker = FormatChecker()
_draft_checkers = dict(
draft3=draft3_format_checker,
draft4=draft4_format_checker,
draft6=draft6_format_checker,
draft7=draft7_format_checker,
+ draft202012=draft202012_format_checker,
)
@@ -147,12 +149,14 @@ def _checks_drafts(
draft4=None,
draft6=None,
draft7=None,
+ draft202012=None,
raises=(),
):
draft3 = draft3 or name
draft4 = draft4 or name
draft6 = draft6 or name
draft7 = draft7 or name
+ draft202012 = draft202012 or name
def wrap(func):
if draft3:
@@ -163,13 +167,17 @@ def _checks_drafts(
func = _draft_checkers["draft6"].checks(draft6, raises)(func)
if draft7:
func = _draft_checkers["draft7"].checks(draft7, raises)(func)
+ if draft202012:
+ func = _draft_checkers["draft202012"].checks(
+ draft202012, raises
+ )(func)
# Oy. This is bad global state, but relied upon for now, until
# deprecation. See https://github.com/Julian/jsonschema/issues/519
# and test_format_checkers_come_with_defaults
- FormatChecker.cls_checks(draft7 or draft6 or draft4 or draft3, raises)(
- func,
- )
+ FormatChecker.cls_checks(
+ draft202012 or draft7 or draft6 or draft4 or draft3, raises
+ )(func)
return func
return wrap
@@ -187,6 +195,7 @@ def is_email(instance):
draft4="ipv4",
draft6="ipv4",
draft7="ipv4",
+ draft202012="ipv4",
raises=ipaddress.AddressValueError,
)
def is_ipv4(instance):
@@ -213,6 +222,7 @@ else:
draft4="hostname",
draft6="hostname",
draft7="hostname",
+ draft202012="hostname",
)
def is_host_name(instance):
if not isinstance(instance, str):
@@ -228,6 +238,7 @@ except ImportError: # pragma: no cover
else:
@_checks_drafts(
draft7="idn-hostname",
+ draft202012="idn-hostname",
raises=(idna.IDNAError, UnicodeError),
)
def is_idn_host_name(instance):
@@ -254,6 +265,7 @@ except ImportError:
@_checks_drafts(
draft6="uri-reference",
draft7="uri-reference",
+ draft202012="uri-reference",
raises=ValueError,
)
def is_uri_reference(instance):
@@ -262,19 +274,30 @@ except ImportError:
return validate_rfc3986(instance, rule="URI_reference")
else:
- @_checks_drafts(draft7="iri", raises=ValueError)
+ @_checks_drafts(
+ draft7="iri",
+ draft202012="iri",
+ raises=ValueError,
+ )
def is_iri(instance):
if not isinstance(instance, str):
return True
return rfc3987.parse(instance, rule="IRI")
- @_checks_drafts(draft7="iri-reference", raises=ValueError)
+ @_checks_drafts(
+ draft7="iri-reference",
+ draft202012="iri-reference",
+ raises=ValueError,
+ )
def is_iri_reference(instance):
if not isinstance(instance, str):
return True
return rfc3987.parse(instance, rule="IRI_reference")
- @_checks_drafts(name="uri", raises=ValueError)
+ @_checks_drafts(
+ name="uri",
+ raises=ValueError,
+ )
def is_uri(instance):
if not isinstance(instance, str):
return True
@@ -283,6 +306,7 @@ else:
@_checks_drafts(
draft6="uri-reference",
draft7="uri-reference",
+ draft202012="uri-reference",
raises=ValueError,
)
def is_uri_reference(instance):
@@ -306,7 +330,10 @@ if validate_rfc3339:
return True
return validate_rfc3339(instance.upper())
- @_checks_drafts(draft7="time")
+ @_checks_drafts(
+ draft7="time",
+ draft202012="time",
+ )
def is_time(instance):
if not isinstance(instance, str):
return True
@@ -327,7 +354,12 @@ else:
return datetime.datetime.strptime(instance, "%Y-%m-%d")
-@_checks_drafts(draft3="date", draft7="date", raises=ValueError)
+@_checks_drafts(
+ draft3="date",
+ draft7="date",
+ draft202012="date",
+ raises=ValueError,
+)
def is_date(instance):
if not isinstance(instance, str):
return True
@@ -377,6 +409,7 @@ else:
@_checks_drafts(
draft6="json-pointer",
draft7="json-pointer",
+ draft202012="json-pointer",
raises=jsonpointer.JsonPointerException,
)
def is_json_pointer(instance):
@@ -390,6 +423,7 @@ else:
# into a new external library.
@_checks_drafts(
draft7="relative-json-pointer",
+ draft202012="relative-json-pointer",
raises=jsonpointer.JsonPointerException,
)
def is_relative_json_pointer(instance):
@@ -400,6 +434,10 @@ else:
non_negative_integer, rest = [], ""
for i, character in enumerate(instance):
if character.isdigit():
+ # digits with a leading "0" are not allowed
+ if i > 0 and int(instance[i-1]) == 0:
+ return False
+
non_negative_integer.append(character)
continue
@@ -419,8 +457,36 @@ else:
@_checks_drafts(
draft6="uri-template",
draft7="uri-template",
+ draft202012="uri-template",
)
def is_uri_template(instance):
if not isinstance(instance, str):
return True
return uri_template.validate(instance)
+
+
+try:
+ import isoduration
+except ImportError: # pragma: no cover
+ pass
+else:
+ @_checks_drafts(
+ draft202012="duration",
+ raises=isoduration.DurationParsingException,
+ )
+ def is_duration(instance):
+ if not isinstance(instance, str):
+ return True
+ return isoduration.parse_duration(instance)
+
+
+@_checks_drafts(
+ draft202012="uuid",
+ raises=ValueError,
+)
+def is_uuid(instance):
+ if not isinstance(instance, str):
+ return True
+ if "-" not in instance:
+ raise ValueError("Invalid UUID format")
+ return UUID(instance)
diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py
index 80f798b..194c68f 100644
--- a/jsonschema/_legacy_validators.py
+++ b/jsonschema/_legacy_validators.py
@@ -2,6 +2,18 @@ from jsonschema import _utils
from jsonschema.exceptions import ValidationError
+def ignore_ref_siblings(schema):
+ """
+ Returns a list of validators that should apply for the given schema
+ Used for draft7 and earlier
+ """
+ ref = schema.get(u"$ref")
+ if ref is not None:
+ return [(u"$ref", ref)]
+ else:
+ return schema.items()
+
+
def dependencies_draft3(validator, dependencies, instance, schema):
if not validator.is_type(instance, "object"):
return
@@ -27,6 +39,37 @@ def dependencies_draft3(validator, dependencies, instance, schema):
yield ValidationError(message % (each, property))
+def dependencies_draft4_draft6_draft7(
+ validator,
+ dependencies,
+ instance,
+ schema,
+):
+ """
+ Support for the ``dependencies`` validator from pre-draft 2019-09.
+
+ In later drafts, the validator was split into separate
+ ``dependentRequired`` and ``dependentSchemas`` validators.
+ """
+ if not validator.is_type(instance, "object"):
+ return
+
+ for property, dependency in dependencies.items():
+ if property not in instance:
+ continue
+
+ if validator.is_type(dependency, "array"):
+ for each in dependency:
+ if each not in instance:
+ message = "%r is a dependency of %r"
+ yield ValidationError(message % (each, property))
+ else:
+ for error in validator.descend(
+ instance, dependency, schema_path=property,
+ ):
+ yield error
+
+
def disallow_draft3(validator, disallow, instance, schema):
for disallowed in _utils.ensure_list(disallow):
if validator.is_valid(instance, {"type": [disallowed]}):
@@ -61,6 +104,22 @@ def items_draft3_draft4(validator, items, instance, schema):
yield error
+def items_draft6_draft7(validator, items, instance, schema):
+ if not validator.is_type(instance, "array"):
+ return
+
+ if validator.is_type(items, "array"):
+ for (index, item), subschema in zip(enumerate(instance), items):
+ for error in validator.descend(
+ item, subschema, path=index, schema_path=index,
+ ):
+ yield error
+ else:
+ for index, item in enumerate(instance):
+ for error in validator.descend(item, items, path=index):
+ yield error
+
+
def minimum_draft3_draft4(validator, minimum, instance, schema):
if not validator.is_type(instance, "number"):
return
@@ -138,3 +197,13 @@ def type_draft3(validator, types, instance, schema):
yield ValidationError(
_utils.types_msg(instance, types), context=all_errors,
)
+
+
+def contains_draft6_draft7(validator, contains, instance, schema):
+ if not validator.is_type(instance, "array"):
+ return
+
+ if not any(validator.is_valid(element, contains) for element in instance):
+ yield ValidationError(
+ "None of %r are valid under the given schema" % (instance,)
+ )
diff --git a/jsonschema/_types.py b/jsonschema/_types.py
index 50bcf99..3eab7b0 100644
--- a/jsonschema/_types.py
+++ b/jsonschema/_types.py
@@ -185,3 +185,4 @@ draft6_type_checker = draft4_type_checker.redefine(
),
)
draft7_type_checker = draft6_type_checker
+draft202012_type_checker = draft7_type_checker
diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py
index 97b7f8f..1b81a09 100644
--- a/jsonschema/_utils.py
+++ b/jsonschema/_utils.py
@@ -1,9 +1,12 @@
from collections.abc import Mapping, MutableMapping, Sequence
+from pathlib import Path
from urllib.parse import urlsplit
import itertools
import json
+import os
import pkgutil
import re
+import sys
class URIDict(MutableMapping):
@@ -55,6 +58,19 @@ def load_schema(name):
return json.loads(data.decode("utf-8"))
+def load_vocabulary(name):
+ """
+ Load all schema files from ./schemas/``name`` and return them as a list.
+ """
+ vocabulary = []
+ base_path = os.path.dirname(sys.modules["jsonschema"].__file__)
+ pathlist = Path(os.path.join(base_path, 'schemas', name)).glob('*.json')
+ for path in pathlist:
+ with open(path) as data:
+ vocabulary.append(json.load(data))
+ return vocabulary
+
+
def format_as_index(indices):
"""
Construct a single string containing indexing operations for the indices.
@@ -232,3 +248,145 @@ def uniq(container):
seen.append(e)
return True
+
+
+def find_evaluated_item_indexes_by_schema(validator, instance, schema):
+ """
+ Get all indexes of items that get evaluated under the current schema
+
+ Covers all keywords related to unevaluatedItems: items, prefixItems, if,
+ then, else, contains, unevaluatedItems, allOf, oneOf, anyOf
+ """
+ if validator.is_type(schema, "boolean"):
+ return []
+ evaluated_indexes = []
+
+ if "items" in schema:
+ return list(range(0, len(instance)))
+
+ if "$ref" in schema:
+ scope, resolved = validator.resolver.resolve(schema["$ref"])
+ validator.resolver.push_scope(scope)
+
+ try:
+ evaluated_indexes += find_evaluated_item_indexes_by_schema(
+ validator, instance, resolved)
+ finally:
+ validator.resolver.pop_scope()
+
+ if "prefixItems" in schema:
+ evaluated_indexes += list(range(0, len(schema["prefixItems"])))
+
+ if "if" in schema:
+ if validator.is_valid(instance, schema["if"]):
+ evaluated_indexes += find_evaluated_item_indexes_by_schema(
+ validator, instance, schema["if"]
+ )
+ if "then" in schema:
+ evaluated_indexes += find_evaluated_item_indexes_by_schema(
+ validator, instance, schema["then"]
+ )
+ else:
+ if "else" in schema:
+ evaluated_indexes += find_evaluated_item_indexes_by_schema(
+ validator, instance, schema["else"]
+ )
+
+ for keyword in ["contains", "unevaluatedItems"]:
+ if keyword in schema:
+ for k, v in enumerate(instance):
+ if validator.is_valid(v, schema[keyword]):
+ evaluated_indexes.append(k)
+
+ for keyword in ["allOf", "oneOf", "anyOf"]:
+ if keyword in schema:
+ for subschema in schema[keyword]:
+ errs = list(validator.descend(instance, subschema))
+ if not errs:
+ evaluated_indexes += find_evaluated_item_indexes_by_schema(
+ validator, instance, subschema
+ )
+
+ return evaluated_indexes
+
+
+def find_evaluated_property_keys_by_schema(validator, instance, schema):
+ """
+ Get all keys of items that get evaluated under the current schema
+
+ Covers all keywords related to unevaluatedProperties: properties,
+ additionalProperties, unevaluatedProperties, patternProperties,
+ dependentSchemas, allOf, oneOf, anyOf, if, then, else
+ """
+ if validator.is_type(schema, "boolean"):
+ return []
+ evaluated_keys = []
+
+ if "$ref" in schema:
+ scope, resolved = validator.resolver.resolve(schema["$ref"])
+ validator.resolver.push_scope(scope)
+
+ try:
+ evaluated_keys += find_evaluated_property_keys_by_schema(
+ validator, instance, resolved
+ )
+ finally:
+ validator.resolver.pop_scope()
+
+ for keyword in [
+ "properties", "additionalProperties", "unevaluatedProperties"
+ ]:
+ if keyword in schema:
+ if validator.is_type(schema[keyword], "boolean"):
+ for property, value in instance.items():
+ if validator.is_valid({property: value}, schema[keyword]):
+ evaluated_keys.append(property)
+
+ if validator.is_type(schema[keyword], "object"):
+ for property, subschema in schema[keyword].items():
+ if property in instance and validator.is_valid(
+ instance[property], subschema
+ ):
+ evaluated_keys.append(property)
+
+ if "patternProperties" in schema:
+ for property, value in instance.items():
+ for pattern, subschema in schema["patternProperties"].items():
+ if re.search(pattern, property) and validator.is_valid(
+ {property: value}, schema["patternProperties"]
+ ):
+ evaluated_keys.append(property)
+
+ if "dependentSchemas" in schema:
+ for property, subschema in schema["dependentSchemas"].items():
+ if property not in instance:
+ continue
+ evaluated_keys += find_evaluated_property_keys_by_schema(
+ validator, instance, subschema
+ )
+
+ for keyword in ["allOf", "oneOf", "anyOf"]:
+ if keyword in schema:
+ for subschema in schema[keyword]:
+ errs = list(validator.descend(instance, subschema))
+ if not errs:
+ evaluated_keys += find_evaluated_property_keys_by_schema(
+ validator, instance, subschema
+ )
+
+ if "if" in schema:
+ if validator.is_valid(instance, schema["if"]):
+ evaluated_keys += find_evaluated_property_keys_by_schema(
+ validator, instance, schema["if"]
+ )
+ if "then" in schema:
+ evaluated_keys += find_evaluated_property_keys_by_schema(
+ validator, instance, schema["then"]
+ )
+ else:
+ if "else" in schema:
+ evaluated_keys += find_evaluated_property_keys_by_schema(
+ validator, instance, schema["else"]
+ )
+
+ return evaluated_keys
diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py
index 0f7b6fb..b6a2f9c 100644
--- a/jsonschema/_validators.py
+++ b/jsonschema/_validators.py
@@ -1,4 +1,5 @@
from fractions import Fraction
+from urllib.parse import urldefrag, urljoin
import re
from jsonschema._utils import (
@@ -6,6 +7,8 @@ from jsonschema._utils import (
equal,
extras_msg,
find_additional_properties,
+ find_evaluated_item_indexes_by_schema,
+ find_evaluated_property_keys_by_schema,
types_msg,
unbool,
uniq,
@@ -70,14 +73,17 @@ def items(validator, items, instance, schema):
if not validator.is_type(instance, "array"):
return
- if validator.is_type(items, "array"):
- for (index, item), subschema in zip(enumerate(instance), items):
- for error in validator.descend(
- item, subschema, path=index, schema_path=index,
- ):
- yield error
+ if validator.is_type(items, "boolean") and 'prefixItems' in schema:
+ if not items:
+ if len(instance) > len(schema['prefixItems']):
+ yield ValidationError(
+ "%r has more items than defined in prefixItems" % instance
+ )
else:
- for index, item in enumerate(instance):
+ non_prefixed_items = instance[len(schema['prefixItems']):] \
+ if 'prefixItems' in schema else instance
+
+ for index, item in enumerate(non_prefixed_items):
for error in validator.descend(item, items, path=index):
yield error
@@ -111,10 +117,56 @@ def contains(validator, contains, instance, schema):
if not validator.is_type(instance, "array"):
return
- if not any(validator.is_valid(element, contains) for element in instance):
+ min_contains = max_contains = None
+
+ if 'minContains' in schema:
+ min_contains = schema['minContains']
+
+ if 'maxContains' in schema:
+ max_contains = schema['maxContains']
+
+ # minContains set to 0 will ignore contains
+ if min_contains == 0:
+ return
+
+ matches = sum(1 for each in instance if validator.is_valid(each, contains))
+
+ # default contains behavior
+ if not matches:
yield ValidationError(
"None of %r are valid under the given schema" % (instance,)
)
+ return
+
+ if min_contains and max_contains is None:
+ if matches < min_contains:
+ yield ValidationError(
+ "Too few matches under the given schema. "
+ "Expected %d but there were only %d." % (
+ min_contains, matches
+ )
+ )
+ return
+
+ if min_contains is None and max_contains:
+ if matches > max_contains:
+ yield ValidationError(
+ "Too many matches under the given schema. "
+ "Expected %d but there were only %d." % (
+ max_contains, matches
+ )
+ )
+ return
+
+ if min_contains and max_contains:
+ if matches < min_contains or matches > max_contains:
+ yield ValidationError(
+ "Invalid number or matches under the given schema, "
+ "expected between %d and %d, got %d" % (
+ min_contains, max_contains, matches
+ )
+ )
+ return
def exclusiveMinimum(validator, minimum, instance, schema):
@@ -233,24 +285,29 @@ def maxLength(validator, mL, instance, schema):
yield ValidationError("%r is too long" % (instance,))
-def dependencies(validator, dependencies, instance, schema):
+def dependentRequired(validator, dependentRequired, instance, schema):
if not validator.is_type(instance, "object"):
return
- for property, dependency in dependencies.items():
+ for property, dependency in dependentRequired.items():
if property not in instance:
continue
- if validator.is_type(dependency, "array"):
- for each in dependency:
- if each not in instance:
- message = "%r is a dependency of %r"
- yield ValidationError(message % (each, property))
- else:
- for error in validator.descend(
+ for each in dependency:
+ if each not in instance:
+ message = "%r is a dependency of %r"
+ yield ValidationError(message % (each, property))
+
+
+def dependentSchemas(validator, dependentSchemas, instance, schema):
+ for property, dependency in dependentSchemas.items():
+ if property not in instance:
+ continue
+
+ for error in validator.descend(
instance, dependency, schema_path=property,
- ):
- yield error
+ ):
+ yield error
def enum(validator, enums, instance, schema):
@@ -279,6 +336,41 @@ def ref(validator, ref, instance, schema):
validator.resolver.pop_scope()
+def dynamicRef(validator, dynamicRef, instance, schema):
+ _, fragment = urldefrag(dynamicRef)
+ scope_stack = validator.resolver.scopes_stack_copy
+
+ for url in scope_stack:
+ lookup_url = urljoin(url, dynamicRef)
+ with validator.resolver.resolving(lookup_url) as lookup_schema:
+ if ("$dynamicAnchor" in lookup_schema
+ and fragment == lookup_schema["$dynamicAnchor"]):
+ subschema = lookup_schema
+ for error in validator.descend(instance, subschema):
+ yield error
+ break
+ else:
+ with validator.resolver.resolving(dynamicRef) as lookup_schema:
+ subschema = lookup_schema
+ for error in validator.descend(instance, subschema):
+ yield error
+
+
+def defs(validator, defs, instance, schema):
+ if not validator.is_type(instance, "object"):
+ return
+
+ if '$defs' in instance:
+ for definition, subschema in instance['$defs'].items():
+ for error in validator.descend(
+ subschema,
+ schema,
+ path=definition,
+ schema_path=definition,
+ ):
+ yield error
+
+
def type(validator, types, instance, schema):
types = ensure_list(types)
@@ -384,3 +476,51 @@ def if_(validator, if_schema, instance, schema):
else_ = schema[u"else"]
for error in validator.descend(instance, else_, schema_path="else"):
yield error
+
+
+def unevaluatedItems(validator, unevaluatedItems, instance, schema):
+ evaluated_item_indexes = find_evaluated_item_indexes_by_schema(
+ validator, instance, schema
+ )
+ unevaluated_items = []
+ for k, v in enumerate(instance):
+ if k not in evaluated_item_indexes:
+ for error in validator.descend(
+ v, unevaluatedItems, schema_path="unevaluatedItems"
+ ):
+ unevaluated_items.append(v)
+
+ if len(unevaluated_items):
+ error = "Unevaluated items are not allowed (%s %s unexpected)"
+ yield ValidationError(error % extras_msg(unevaluated_items))
+
+
+def unevaluatedProperties(validator, unevaluatedProperties, instance, schema):
+ evaluated_property_keys = find_evaluated_property_keys_by_schema(
+ validator, instance, schema
+ )
+ unevaluated_property_keys = []
+ for property, subschema in instance.items():
+ if property not in evaluated_property_keys:
+ for error in validator.descend(
+ instance[property],
+ unevaluatedProperties,
+ path=property,
+ schema_path=property,
+ ):
+ unevaluated_property_keys.append(property)
+
+ if len(unevaluated_property_keys):
+ error = "Unevaluated properties are not allowed (%s %s unexpected)"
+ yield ValidationError(error % extras_msg(unevaluated_property_keys))
+
+
+def prefixItems(validator, prefixItems, instance, schema):
+ if not validator.is_type(instance, "array"):
+ return
+
+ for k, v in enumerate(instance[:min(len(prefixItems), len(instance))]):
+ for error in validator.descend(
+ v, prefixItems[k], schema_path="prefixItems"
+ ):
+ yield error
diff --git a/jsonschema/schemas/draft2020-12.json b/jsonschema/schemas/draft2020-12.json
new file mode 100644
index 0000000..d5e2d31
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12.json
@@ -0,0 +1,58 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/schema",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/core": true,
+ "https://json-schema.org/draft/2020-12/vocab/applicator": true,
+ "https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
+ "https://json-schema.org/draft/2020-12/vocab/validation": true,
+ "https://json-schema.org/draft/2020-12/vocab/meta-data": true,
+ "https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
+ "https://json-schema.org/draft/2020-12/vocab/content": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Core and Validation specifications meta-schema",
+ "allOf": [
+ {"$ref": "meta/core"},
+ {"$ref": "meta/applicator"},
+ {"$ref": "meta/unevaluated"},
+ {"$ref": "meta/validation"},
+ {"$ref": "meta/meta-data"},
+ {"$ref": "meta/format-annotation"},
+ {"$ref": "meta/content"}
+ ],
+ "type": ["object", "boolean"],
+ "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.",
+ "properties": {
+ "definitions": {
+ "$comment": "\"definitions\" has been replaced by \"$defs\".",
+ "type": "object",
+ "additionalProperties": { "$dynamicRef": "#meta" },
+ "deprecated": true,
+ "default": {}
+ },
+ "dependencies": {
+ "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ { "$dynamicRef": "#meta" },
+ { "$ref": "meta/validation#/$defs/stringArray" }
+ ]
+ },
+ "deprecated": true,
+ "default": {}
+ },
+ "$recursiveAnchor": {
+ "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".",
+ "$ref": "meta/core#/$defs/anchorString",
+ "deprecated": true
+ },
+ "$recursiveRef": {
+ "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".",
+ "$ref": "meta/core#/$defs/uriReferenceString",
+ "deprecated": true
+ }
+ }
+}
diff --git a/jsonschema/schemas/draft2020-12/applicator.json b/jsonschema/schemas/draft2020-12/applicator.json
new file mode 100644
index 0000000..ca69923
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12/applicator.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/meta/applicator",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/applicator": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Applicator vocabulary meta-schema",
+ "type": ["object", "boolean"],
+ "properties": {
+ "prefixItems": { "$ref": "#/$defs/schemaArray" },
+ "items": { "$dynamicRef": "#meta" },
+ "contains": { "$dynamicRef": "#meta" },
+ "additionalProperties": { "$dynamicRef": "#meta" },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$dynamicRef": "#meta" },
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": { "$dynamicRef": "#meta" },
+ "propertyNames": { "format": "regex" },
+ "default": {}
+ },
+ "dependentSchemas": {
+ "type": "object",
+ "additionalProperties": { "$dynamicRef": "#meta" },
+ "default": {}
+ },
+ "propertyNames": { "$dynamicRef": "#meta" },
+ "if": { "$dynamicRef": "#meta" },
+ "then": { "$dynamicRef": "#meta" },
+ "else": { "$dynamicRef": "#meta" },
+ "allOf": { "$ref": "#/$defs/schemaArray" },
+ "anyOf": { "$ref": "#/$defs/schemaArray" },
+ "oneOf": { "$ref": "#/$defs/schemaArray" },
+ "not": { "$dynamicRef": "#meta" }
+ },
+ "$defs": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$dynamicRef": "#meta" }
+ }
+ }
+}
diff --git a/jsonschema/schemas/draft2020-12/content.json b/jsonschema/schemas/draft2020-12/content.json
new file mode 100644
index 0000000..2f6e056
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12/content.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/meta/content",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/content": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Content vocabulary meta-schema",
+
+ "type": ["object", "boolean"],
+ "properties": {
+ "contentEncoding": { "type": "string" },
+ "contentMediaType": { "type": "string" },
+ "contentSchema": { "$dynamicRef": "#meta" }
+ }
+}
diff --git a/jsonschema/schemas/draft2020-12/core.json b/jsonschema/schemas/draft2020-12/core.json
new file mode 100644
index 0000000..dfc092d
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12/core.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/meta/core",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/core": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Core vocabulary meta-schema",
+ "type": ["object", "boolean"],
+ "properties": {
+ "$id": {
+ "$ref": "#/$defs/uriReferenceString",
+ "$comment": "Non-empty fragments not allowed.",
+ "pattern": "^[^#]*#?$"
+ },
+ "$schema": { "$ref": "#/$defs/uriString" },
+ "$ref": { "$ref": "#/$defs/uriReferenceString" },
+ "$anchor": { "$ref": "#/$defs/anchorString" },
+ "$dynamicRef": { "$ref": "#/$defs/uriReferenceString" },
+ "$dynamicAnchor": { "$ref": "#/$defs/anchorString" },
+ "$vocabulary": {
+ "type": "object",
+ "propertyNames": { "$ref": "#/$defs/uriString" },
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "$comment": {
+ "type": "string"
+ },
+ "$defs": {
+ "type": "object",
+ "additionalProperties": { "$dynamicRef": "#meta" }
+ }
+ },
+ "$defs": {
+ "anchorString": {
+ "type": "string",
+ "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$"
+ },
+ "uriString": {
+ "type": "string",
+ "format": "uri"
+ },
+ "uriReferenceString": {
+ "type": "string",
+ "format": "uri-reference"
+ }
+ }
+}
diff --git a/jsonschema/schemas/draft2020-12/format-annotation.json b/jsonschema/schemas/draft2020-12/format-annotation.json
new file mode 100644
index 0000000..51ef7ea
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12/format-annotation.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/format-annotation": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Format vocabulary meta-schema for annotation results",
+ "type": ["object", "boolean"],
+ "properties": {
+ "format": { "type": "string" }
+ }
+}
diff --git a/jsonschema/schemas/draft2020-12/meta-data.json b/jsonschema/schemas/draft2020-12/meta-data.json
new file mode 100644
index 0000000..05cbc22
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12/meta-data.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/meta/meta-data",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/meta-data": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Meta-data vocabulary meta-schema",
+
+ "type": ["object", "boolean"],
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "default": true,
+ "deprecated": {
+ "type": "boolean",
+ "default": false
+ },
+ "readOnly": {
+ "type": "boolean",
+ "default": false
+ },
+ "writeOnly": {
+ "type": "boolean",
+ "default": false
+ },
+ "examples": {
+ "type": "array",
+ "items": true
+ }
+ }
+}
diff --git a/jsonschema/schemas/draft2020-12/unevaluated.json b/jsonschema/schemas/draft2020-12/unevaluated.json
new file mode 100644
index 0000000..5f62a3f
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12/unevaluated.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/unevaluated": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Unevaluated applicator vocabulary meta-schema",
+ "type": ["object", "boolean"],
+ "properties": {
+ "unevaluatedItems": { "$dynamicRef": "#meta" },
+ "unevaluatedProperties": { "$dynamicRef": "#meta" }
+ }
+}
diff --git a/jsonschema/schemas/draft2020-12/validation.json b/jsonschema/schemas/draft2020-12/validation.json
new file mode 100644
index 0000000..606b87b
--- /dev/null
+++ b/jsonschema/schemas/draft2020-12/validation.json
@@ -0,0 +1,98 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://json-schema.org/draft/2020-12/meta/validation",
+ "$vocabulary": {
+ "https://json-schema.org/draft/2020-12/vocab/validation": true
+ },
+ "$dynamicAnchor": "meta",
+
+ "title": "Validation vocabulary meta-schema",
+ "type": ["object", "boolean"],
+ "properties": {
+ "type": {
+ "anyOf": [
+ { "$ref": "#/$defs/simpleTypes" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/$defs/simpleTypes" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ },
+ "const": true,
+ "enum": {
+ "type": "array",
+ "items": true
+ },
+ "multipleOf": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "number"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "number"
+ },
+ "maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
+ "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
+ "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": false
+ },
+ "maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
+ "minContains": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "default": 1
+ },
+ "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
+ "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
+ "required": { "$ref": "#/$defs/stringArray" },
+ "dependentRequired": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/stringArray"
+ }
+ }
+ },
+ "$defs": {
+ "nonNegativeInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "nonNegativeIntegerDefault0": {
+ "$ref": "#/$defs/nonNegativeInteger",
+ "default": 0
+ },
+ "simpleTypes": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "null",
+ "number",
+ "object",
+ "string"
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": { "type": "string" },
+ "uniqueItems": true,
+ "default": []
+ }
+ }
+}
diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py
index 884a037..ae4f13d 100644
--- a/jsonschema/tests/test_cli.py
+++ b/jsonschema/tests/test_cli.py
@@ -10,7 +10,7 @@ import subprocess
import sys
import tempfile
-from jsonschema import Draft4Validator, Draft7Validator, __version__, cli
+from jsonschema import Draft4Validator, Draft202012Validator, __version__, cli
from jsonschema.exceptions import (
RefResolutionError,
SchemaError,
@@ -336,7 +336,7 @@ class TestCLI(TestCase):
exit_code=1,
stderr="""\
- 57: 57 is not valid under any of the given schemas
+ 57: 57 is not of type 'object', 'boolean'
""",
)
@@ -776,7 +776,7 @@ class TestCLI(TestCase):
# is hidden inside the CLI, so guard that that's the case, and
# this test will have to be updated when versions change until
# we can think of a better way to ensure this behavior.
- self.assertIs(Draft7Validator, _LATEST_VERSION)
+ self.assertIs(Draft202012Validator, _LATEST_VERSION)
self.assertOutputs(
files=dict(some_schema='{"const": "check"}', some_instance='"a"'),
diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py
index 6dba484..06f841c 100644
--- a/jsonschema/tests/test_format.py
+++ b/jsonschema/tests/test_format.py
@@ -79,10 +79,29 @@ class TestFormatChecker(TestCase):
def test_repr(self):
checker = FormatChecker(formats=())
- checker.checks("foo")(lambda thing: True)
- checker.checks("bar")(lambda thing: True)
- checker.checks("baz")(lambda thing: True)
+ checker.checks("foo")(lambda thing: True) # pragma: no cover
+ checker.checks("bar")(lambda thing: True) # pragma: no cover
+ checker.checks("baz")(lambda thing: True) # pragma: no cover
self.assertEqual(
repr(checker),
"<FormatChecker checkers=['bar', 'baz', 'foo']>",
)
+
+ def test_duration_format(self):
+ try:
+ from jsonschema._format import is_duration # noqa: F401
+ except ImportError: # pragma: no cover
+ pass
+ else:
+ checker = FormatChecker()
+ self.assertTrue(checker.conforms(1, "duration"))
+ self.assertTrue(checker.conforms("P4Y", "duration"))
+ self.assertFalse(checker.conforms("test", "duration"))
+
+ def test_uuid_format(self):
+ checker = FormatChecker()
+ self.assertTrue(checker.conforms(1, "uuid"))
+ self.assertTrue(
+ checker.conforms("6e6659ec-4503-4428-9f03-2e2ea4d6c278", "uuid")
+ )
+ self.assertFalse(checker.conforms("test", "uuid"))
diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py
index 40e3d85..b1e0723 100644
--- a/jsonschema/tests/test_jsonschema_test_suite.py
+++ b/jsonschema/tests/test_jsonschema_test_suite.py
@@ -13,10 +13,12 @@ from jsonschema import (
Draft4Validator,
Draft6Validator,
Draft7Validator,
+ Draft202012Validator,
draft3_format_checker,
draft4_format_checker,
draft6_format_checker,
draft7_format_checker,
+ draft202012_format_checker,
)
from jsonschema.tests._helpers import bug
from jsonschema.tests._suite import Suite
@@ -26,6 +28,7 @@ DRAFT3 = SUITE.version(name="draft3")
DRAFT4 = SUITE.version(name="draft4")
DRAFT6 = SUITE.version(name="draft6")
DRAFT7 = SUITE.version(name="draft7")
+DRAFT202012 = SUITE.version(name="draft2020-12")
def skip(message, **kwargs):
@@ -398,3 +401,63 @@ TestDraft7 = DRAFT7.to_unittest_testcase(
)(test)
),
)
+
+
+TestDraft202012 = DRAFT202012.to_unittest_testcase(
+ DRAFT202012.tests(),
+ DRAFT202012.optional_tests_of(name="bignum"),
+ DRAFT202012.optional_tests_of(name="float-overflow"),
+ DRAFT202012.optional_tests_of(name="non-bmp-regex"),
+ DRAFT202012.optional_tests_of(name="refOfUnknownKeyword"),
+ Validator=Draft202012Validator,
+ skip=lambda test: (
+ narrow_unicode_build(test)
+ or skip(
+ message="Issue: Resolving of dynamicRef based on dynamic scope",
+ subject="dynamicRef",
+ case_description="A $dynamicRef that initially resolves to a "
+ "schema with a matching $dynamicAnchor should "
+ "resolve to the first $dynamicAnchor in the "
+ "dynamic scope",
+ description='The recursive part is not valid against the root',
+ )(test)
+ or skip(
+ message="Issue: Resolving of dynamicRef based on dynamic scope",
+ subject="dynamicRef",
+ case_description="multiple dynamic paths to the $dynamicRef "
+ "keyword",
+ description="recurse to integerNode - floats are not allowed",
+ )(test)
+ or skip(
+ message="Issue: Resolving of dynamicRef based on dynamic scope",
+ subject="dynamicRef",
+ case_description="after leaving a dynamic scope, it should not be "
+ "used by a $dynamicRef",
+ description="/then/$defs/thingy is the final stop for the "
+ "$dynamicRef",
+ )(test)
+ or skip(
+ message="Issue: Resolving of dynamicRef based on dynamic scope",
+ subject="dynamicRef",
+ case_description="after leaving a dynamic scope, it should not be "
+ 'used by a $dynamicRef',
+ description="string matches /$defs/thingy, but the $dynamicRef "
+ "does not stop here",
+ )(test)
+ ),
+)
+
+
+TestDraft202012Format = DRAFT202012.to_unittest_testcase(
+ DRAFT202012.format_tests(),
+ Validator=Draft202012Validator,
+ format_checker=draft202012_format_checker,
+ skip=lambda test: (
+ complex_email_validation(test)
+ or missing_date_fromisoformat(test)
+ or allowed_leading_zeros(test)
+ or leap_second(test)
+ or missing_format(draft202012_format_checker)(test)
+ or complex_email_validation(test)
+ )
+)
diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py
index af5dfeb..bb3fefb 100644
--- a/jsonschema/tests/test_validators.py
+++ b/jsonschema/tests/test_validators.py
@@ -383,6 +383,40 @@ class TestValidationErrorMessages(TestCase):
)
self.assertIn("False schema does not allow 'something'", message)
+ def test_unevaluated_properties(self):
+ schema = {
+ "type": "object",
+ "unevaluatedProperties": False
+ }
+ message = self.message_for(
+ instance={
+ "foo": "foo",
+ "bar": "bar",
+ },
+ schema=schema,
+ cls=validators.Draft202012Validator,
+ )
+ self.assertIn(
+ "Unevaluated properties are not allowed "
+ "('foo', 'bar' were unexpected)",
+ message,
+ )
+
+ def test_unevaluated_items(self):
+ schema = {
+ "type": "array",
+ "unevaluatedItems": False
+ }
+ message = self.message_for(
+ instance=["foo", "bar"],
+ schema=schema,
+ cls=validators.Draft202012Validator,
+ )
+ self.assertIn(
+ "Unevaluated items are not allowed ('foo', 'bar' were unexpected)",
+ message,
+ )
+
class TestValidationErrorDetails(TestCase):
# TODO: These really need unit tests for each individual validator, rather
@@ -1244,6 +1278,12 @@ class TestDraft7Validator(ValidatorTestMixin, TestCase):
invalid = {"type": "integer"}, "foo"
+class TestDraft202012Validator(ValidatorTestMixin, TestCase):
+ Validator = validators.Draft202012Validator
+ valid = {}, {}
+ invalid = {"type": "integer"}, "foo"
+
+
class TestValidatorFor(SynchronousTestCase):
def test_draft_3(self):
schema = {"$schema": "http://json-schema.org/draft-03/schema"}
@@ -1297,6 +1337,19 @@ class TestValidatorFor(SynchronousTestCase):
validators.Draft7Validator,
)
+ def test_draft_202012(self):
+ schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
+ self.assertIs(
+ validators.validator_for(schema),
+ validators.Draft202012Validator,
+ )
+
+ schema = {"$schema": "https://json-schema.org/draft/2020-12/schema#"}
+ self.assertIs(
+ validators.validator_for(schema),
+ validators.Draft202012Validator,
+ )
+
def test_True(self):
self.assertIs(
validators.validator_for(True),
@@ -1410,8 +1463,23 @@ class TestValidate(SynchronousTestCase):
Validator=validators.Draft7Validator,
)
- def test_draft7_validator_is_the_default(self):
- self.assertUses(schema={}, Validator=validators.Draft7Validator)
+ def test_draft202012_validator_is_chosen(self):
+ self.assertUses(
+ schema={
+ "$schema": "https://json-schema.org/draft/2020-12/schema#"
+ },
+ Validator=validators.Draft202012Validator,
+ )
+ # Make sure it works without the empty fragment
+ self.assertUses(
+ schema={
+ "$schema": "https://json-schema.org/draft/2020-12/schema"
+ },
+ Validator=validators.Draft202012Validator,
+ )
+
+ def test_draft202012_validator_is_the_default(self):
+ self.assertUses(schema={}, Validator=validators.Draft202012Validator)
def test_validation_error_message(self):
with self.assertRaises(exceptions.ValidationError) as e:
diff --git a/jsonschema/validators.py b/jsonschema/validators.py
index 70d46c2..a1d4214 100644
--- a/jsonschema/validators.py
+++ b/jsonschema/validators.py
@@ -26,6 +26,7 @@ ErrorTree
validators = {}
meta_schemas = _utils.URIDict()
+_VOCABULARIES = _utils.URIDict()
def validates(version):
@@ -51,8 +52,12 @@ def validates(version):
def _validates(cls):
validators[version] = cls
meta_schema_id = cls.ID_OF(cls.META_SCHEMA)
- if meta_schema_id:
- meta_schemas[meta_schema_id] = cls
+ meta_schemas[meta_schema_id] = cls
+
+ for vocabulary in cls.VOCABULARY_SCHEMAS:
+ vocabulary_id = cls.ID_OF(vocabulary)
+ _VOCABULARIES[vocabulary_id] = vocabulary
+
return cls
return _validates
@@ -63,12 +68,22 @@ def _id_of(schema):
return schema.get(u"$id", u"")
+def _store_schema_list():
+ return [
+ (id, validator.META_SCHEMA) for id, validator in meta_schemas.items()
+ ] + [
+ (id, schema) for id, schema in _VOCABULARIES.items()
+ ]
+
+
def create(
meta_schema,
+ vocabulary_schemas=(),
validators=(),
version=None,
type_checker=_types.draft7_type_checker,
id_of=_id_of,
+ applicable_validators=lambda schema: schema.items(),
):
"""
Create a new validator class.
@@ -111,6 +126,11 @@ def create(
A function that given a schema, returns its ID.
+ applicable_validators (collections.abc.Callable):
+
+ A function that returns a list of validators that should apply
+ to a given schema
+
Returns:
a new `jsonschema.IValidator` class
@@ -120,6 +140,7 @@ def create(
VALIDATORS = dict(validators)
META_SCHEMA = dict(meta_schema)
+ VOCABULARY_SCHEMAS = list(vocabulary_schemas)
TYPE_CHECKER = type_checker
ID_OF = staticmethod(id_of)
@@ -161,12 +182,7 @@ def create(
if scope:
self.resolver.push_scope(scope)
try:
- ref = _schema.get(u"$ref")
- if ref is not None:
- validators = [(u"$ref", ref)]
- else:
- validators = _schema.items()
-
+ validators = applicable_validators(_schema)
for k, v in validators:
validator = self.VALIDATORS.get(k)
if validator is None:
@@ -312,6 +328,7 @@ Draft3Validator = create(
type_checker=_types.draft3_type_checker,
version="draft3",
id_of=lambda schema: schema.get(u"id", ""),
+ applicable_validators=_legacy_validators.ignore_ref_siblings,
)
Draft4Validator = create(
@@ -322,7 +339,7 @@ Draft4Validator = create(
u"additionalProperties": _validators.additionalProperties,
u"allOf": _validators.allOf,
u"anyOf": _validators.anyOf,
- u"dependencies": _validators.dependencies,
+ u"dependencies": _legacy_validators.dependencies_draft4_draft6_draft7,
u"enum": _validators.enum,
u"format": _validators.format,
u"items": _legacy_validators.items_draft3_draft4,
@@ -347,6 +364,7 @@ Draft4Validator = create(
type_checker=_types.draft4_type_checker,
version="draft4",
id_of=lambda schema: schema.get(u"id", ""),
+ applicable_validators=_legacy_validators.ignore_ref_siblings,
)
Draft6Validator = create(
@@ -358,13 +376,13 @@ Draft6Validator = create(
u"allOf": _validators.allOf,
u"anyOf": _validators.anyOf,
u"const": _validators.const,
- u"contains": _validators.contains,
- u"dependencies": _validators.dependencies,
+ u"contains": _legacy_validators.contains_draft6_draft7,
+ u"dependencies": _legacy_validators.dependencies_draft4_draft6_draft7,
u"enum": _validators.enum,
u"exclusiveMaximum": _validators.exclusiveMaximum,
u"exclusiveMinimum": _validators.exclusiveMinimum,
u"format": _validators.format,
- u"items": _validators.items,
+ u"items": _legacy_validators.items_draft6_draft7,
u"maxItems": _validators.maxItems,
u"maxLength": _validators.maxLength,
u"maxProperties": _validators.maxProperties,
@@ -386,6 +404,7 @@ Draft6Validator = create(
},
type_checker=_types.draft6_type_checker,
version="draft6",
+ applicable_validators=_legacy_validators.ignore_ref_siblings,
)
Draft7Validator = create(
@@ -397,14 +416,14 @@ Draft7Validator = create(
u"allOf": _validators.allOf,
u"anyOf": _validators.anyOf,
u"const": _validators.const,
- u"contains": _validators.contains,
- u"dependencies": _validators.dependencies,
+ u"contains": _legacy_validators.contains_draft6_draft7,
+ u"dependencies": _legacy_validators.dependencies_draft4_draft6_draft7,
u"enum": _validators.enum,
u"exclusiveMaximum": _validators.exclusiveMaximum,
u"exclusiveMinimum": _validators.exclusiveMinimum,
u"format": _validators.format,
u"if": _validators.if_,
- u"items": _validators.items,
+ u"items": _legacy_validators.items_draft6_draft7,
u"maxItems": _validators.maxItems,
u"maxLength": _validators.maxLength,
u"maxProperties": _validators.maxProperties,
@@ -426,9 +445,57 @@ Draft7Validator = create(
},
type_checker=_types.draft7_type_checker,
version="draft7",
+ applicable_validators=_legacy_validators.ignore_ref_siblings,
)
-_LATEST_VERSION = Draft7Validator
+Draft202012Validator = create(
+ meta_schema=_utils.load_schema("draft2020-12"),
+ vocabulary_schemas=_utils.load_vocabulary("draft2020-12"),
+ validators={
+ u"$ref": _validators.ref,
+ u"$defs": _validators.defs,
+ u"$dynamicRef": _validators.dynamicRef,
+ u"additionalItems": _validators.additionalItems,
+ u"additionalProperties": _validators.additionalProperties,
+ u"allOf": _validators.allOf,
+ u"anyOf": _validators.anyOf,
+ u"const": _validators.const,
+ u"contains": _validators.contains,
+ u"dependentRequired": _validators.dependentRequired,
+ u"dependentSchemas": _validators.dependentSchemas,
+ u"enum": _validators.enum,
+ u"exclusiveMaximum": _validators.exclusiveMaximum,
+ u"exclusiveMinimum": _validators.exclusiveMinimum,
+ u"format": _validators.format,
+ u"if": _validators.if_,
+ u"items": _validators.items,
+ u"maxItems": _validators.maxItems,
+ u"maxLength": _validators.maxLength,
+ u"maxProperties": _validators.maxProperties,
+ u"maximum": _validators.maximum,
+ u"minItems": _validators.minItems,
+ u"minLength": _validators.minLength,
+ u"minProperties": _validators.minProperties,
+ u"minimum": _validators.minimum,
+ u"multipleOf": _validators.multipleOf,
+ u"oneOf": _validators.oneOf,
+ u"not": _validators.not_,
+ u"pattern": _validators.pattern,
+ u"patternProperties": _validators.patternProperties,
+ u"properties": _validators.properties,
+ u"propertyNames": _validators.propertyNames,
+ u"required": _validators.required,
+ u"type": _validators.type,
+ u"uniqueItems": _validators.uniqueItems,
+ u"unevaluatedItems": _validators.unevaluatedItems,
+ u"unevaluatedProperties": _validators.unevaluatedProperties,
+ u"prefixItems": _validators.prefixItems,
+ },
+ type_checker=_types.draft202012_type_checker,
+ version="draft2020-12",
+)
+
+_LATEST_VERSION = Draft202012Validator
class RefResolver(object):
@@ -495,10 +562,7 @@ class RefResolver(object):
self.handlers = dict(handlers)
self._scopes_stack = [base_uri]
- self.store = _utils.URIDict(
- (id, validator.META_SCHEMA)
- for id, validator in meta_schemas.items()
- )
+ self.store = _utils.URIDict(_store_schema_list())
self.store.update(store)
self.store[base_uri] = referrer
@@ -561,6 +625,13 @@ class RefResolver(object):
return self._scopes_stack[-1]
@property
+ def scopes_stack_copy(self):
+ """
+ Retrieve a copy of the stack of resolution scopes.
+ """
+ return self._scopes_stack.copy()
+
+ @property
def base_uri(self):
"""
Retrieve the current base URI, not including any fragment.
@@ -600,11 +671,42 @@ class RefResolver(object):
finally:
self.pop_scope()
+ def _finditem(self, schema, key):
+ results = []
+ if isinstance(schema, dict):
+ if key in schema:
+ results.append(schema)
+
+ for k, v in schema.items():
+ if isinstance(v, dict):
+ results += self._finditem(v, key)
+
+ return results
+
+ def resolve_local(self, url, schema):
+ """
+ Resolve the given reference within the schema
+ """
+ uri, fragment = urldefrag(url)
+
+ for subschema in self._finditem(schema, "$id"):
+ target_uri = self._urljoin_cache(
+ self.resolution_scope, subschema['$id']
+ )
+ if target_uri.rstrip("/") == uri.rstrip("/"):
+ if fragment:
+ subschema = self.resolve_fragment(subschema, fragment)
+ return subschema
+
def resolve(self, ref):
"""
Resolve the given reference.
"""
- url = self._urljoin_cache(self.resolution_scope, ref)
+ url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/")
+ local_resolve = self.resolve_local(url, self.referrer)
+
+ if local_resolve:
+ return url, local_resolve
return url, self._remote_cache(url)
def resolve_from_url(self, url):
@@ -638,8 +740,16 @@ class RefResolver(object):
"""
fragment = fragment.lstrip(u"/")
- parts = unquote(fragment).split(u"/") if fragment else []
+ # Resolve fragment via $anchor or $dynamicAnchor
+ if fragment:
+ for keyword in ["$anchor", "$dynamicAnchor"]:
+ for subschema in self._finditem(document, keyword):
+ if fragment == subschema[keyword]:
+ return subschema
+
+ # Resolve via path
+ parts = unquote(fragment).split(u"/") if fragment else []
for part in parts:
part = part.replace(u"~1", u"/").replace(u"~0", u"~")
diff --git a/setup.cfg b/setup.cfg
index 8806726..d78daaf 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -39,6 +39,7 @@ format =
strict-rfc3339
webcolors
uri_template
+ isoduration;python_version>'3.6'
format_nongpl =
fqdn
idna
@@ -47,13 +48,14 @@ format_nongpl =
rfc3986-validator>0.1.0
rfc3339-validator
uri_template
+ isoduration;python_version>'3.6'
[options.entry_points]
console_scripts =
jsonschema = jsonschema.cli:main
[options.package_data]
-jsonschema = schemas/*.json
+jsonschema = schemas/*.json, schemas/*/*.json
[bdist_wheel]
universal = 1