Skip to content

Commit

Permalink
Consider properties evaluated when they're behind dynamic refs.
Browse files Browse the repository at this point in the history
This was previously correct for $refs, but not $dynamicRefs,
which had no test in the JSON Schema test suite.

This behavior is now properly compliant with the 2020 spec (as well as
2019, for $recursiveRef).

Refs: json-schema-org/JSON-Schema-Test-Suite#696
  • Loading branch information
Julian committed Nov 16, 2023
1 parent 13bc188 commit 5ff5999
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
@@ -1,6 +1,7 @@
v4.20.0
=======

* Properly consider items (and properties) to be evaluated by ``unevaluatedItems`` (resp. ``unevaluatedProperties``) when behind a ``$dynamicRef`` as specified by the 2020 and 2019 specifications.
* ``jsonschema.exceptions.ErrorTree.__setitem__`` is now deprecated.
More broadly, in general users of ``jsonschema`` should never be mutating objects owned by the library.

Expand Down
138 changes: 136 additions & 2 deletions jsonschema/_legacy_keywords.py
@@ -1,3 +1,5 @@
import re

from referencing.jsonschema import lookup_recursive_ref

from jsonschema import _utils
Expand Down Expand Up @@ -249,8 +251,22 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
return []
evaluated_indexes = []

if "$ref" in schema:
resolved = validator._resolver.lookup(schema["$ref"])
ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)

if "$recursiveRef" in schema:
resolved = lookup_recursive_ref(validator._resolver)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
Expand Down Expand Up @@ -316,3 +332,121 @@ def unevaluatedItems_draft2019(validator, unevaluatedItems, instance, schema):
if unevaluated_items:
error = "Unevaluated items are not allowed (%s %s unexpected)"
yield ValidationError(error % _utils.extras_msg(unevaluated_items))


def find_evaluated_property_keys_by_schema(validator, instance, schema):
if validator.is_type(schema, "boolean"):
return []
evaluated_keys = []

ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)

if "$recursiveRef" in schema:
resolved = lookup_recursive_ref(validator._resolver)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)

for keyword in [
"properties", "additionalProperties", "unevaluatedProperties",
]:
if keyword in schema:
schema_value = schema[keyword]
if validator.is_type(schema_value, "boolean") and schema_value:
evaluated_keys += instance.keys()

elif validator.is_type(schema_value, "object"):
for property in schema_value:
if property in instance:
evaluated_keys.append(property)

if "patternProperties" in schema:
for property in instance:
for pattern in schema["patternProperties"]:
if re.search(pattern, property):
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 = next(validator.descend(instance, subschema), None)
if errs is None:
evaluated_keys += find_evaluated_property_keys_by_schema(
validator, instance, subschema,
)

if "if" in schema:
if validator.evolve(schema=schema["if"]).is_valid(instance):
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


def unevaluatedProperties_draft2019(validator, uP, instance, schema):
if not validator.is_type(instance, "object"):
return
evaluated_keys = find_evaluated_property_keys_by_schema(
validator, instance, schema,
)
unevaluated_keys = []
for property in instance:
if property not in evaluated_keys:
for _ in validator.descend(
instance[property],
uP,
path=property,
schema_path=property,
):
# FIXME: Include context for each unevaluated property
# indicating why it's invalid under the subschema.
unevaluated_keys.append(property)

if unevaluated_keys:
if uP is False:
error = "Unevaluated properties are not allowed (%s %s unexpected)"
extras = sorted(unevaluated_keys, key=str)
yield ValidationError(error % _utils.extras_msg(extras))
else:
error = (
"Unevaluated properties are not valid under "
"the given schema (%s %s unevaluated and invalid)"
)
yield ValidationError(error % _utils.extras_msg(unevaluated_keys))
38 changes: 34 additions & 4 deletions jsonschema/_utils.py
Expand Up @@ -197,8 +197,23 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
if "items" in schema:
return list(range(0, len(instance)))

if "$ref" in schema:
resolved = validator._resolver.lookup(schema["$ref"])
ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)

dynamicRef = schema.get("$dynamicRef")
if dynamicRef is not None:
resolved = validator._resolver.lookup(dynamicRef)
evaluated_indexes.extend(
find_evaluated_item_indexes_by_schema(
validator.evolve(
Expand Down Expand Up @@ -258,8 +273,23 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
return []
evaluated_keys = []

if "$ref" in schema:
resolved = validator._resolver.lookup(schema["$ref"])
ref = schema.get("$ref")
if ref is not None:
resolved = validator._resolver.lookup(ref)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
schema=resolved.contents,
_resolver=resolved.resolver,
),
instance,
resolved.contents,
),
)

dynamicRef = schema.get("$dynamicRef")
if dynamicRef is not None:
resolved = validator._resolver.lookup(dynamicRef)
evaluated_keys.extend(
find_evaluated_property_keys_by_schema(
validator.evolve(
Expand Down
4 changes: 3 additions & 1 deletion jsonschema/validators.py
Expand Up @@ -782,7 +782,9 @@ def extend(
"required": _keywords.required,
"type": _keywords.type,
"unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019,
"unevaluatedProperties": _keywords.unevaluatedProperties,
"unevaluatedProperties": (
_legacy_keywords.unevaluatedProperties_draft2019
),
"uniqueItems": _keywords.uniqueItems,
},
type_checker=_types.draft201909_type_checker,
Expand Down

0 comments on commit 5ff5999

Please sign in to comment.