Skip to content

Commit 5ff5999

Browse files
committed
Consider properties evaluated when they're behind dynamic refs.
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
1 parent 13bc188 commit 5ff5999

File tree

4 files changed

+174
-7
lines changed

4 files changed

+174
-7
lines changed

CHANGELOG.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
v4.20.0
22
=======
33

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

jsonschema/_legacy_keywords.py

+136-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
from referencing.jsonschema import lookup_recursive_ref
24

35
from jsonschema import _utils
@@ -249,8 +251,22 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
249251
return []
250252
evaluated_indexes = []
251253

252-
if "$ref" in schema:
253-
resolved = validator._resolver.lookup(schema["$ref"])
254+
ref = schema.get("$ref")
255+
if ref is not None:
256+
resolved = validator._resolver.lookup(ref)
257+
evaluated_indexes.extend(
258+
find_evaluated_item_indexes_by_schema(
259+
validator.evolve(
260+
schema=resolved.contents,
261+
_resolver=resolved.resolver,
262+
),
263+
instance,
264+
resolved.contents,
265+
),
266+
)
267+
268+
if "$recursiveRef" in schema:
269+
resolved = lookup_recursive_ref(validator._resolver)
254270
evaluated_indexes.extend(
255271
find_evaluated_item_indexes_by_schema(
256272
validator.evolve(
@@ -316,3 +332,121 @@ def unevaluatedItems_draft2019(validator, unevaluatedItems, instance, schema):
316332
if unevaluated_items:
317333
error = "Unevaluated items are not allowed (%s %s unexpected)"
318334
yield ValidationError(error % _utils.extras_msg(unevaluated_items))
335+
336+
337+
def find_evaluated_property_keys_by_schema(validator, instance, schema):
338+
if validator.is_type(schema, "boolean"):
339+
return []
340+
evaluated_keys = []
341+
342+
ref = schema.get("$ref")
343+
if ref is not None:
344+
resolved = validator._resolver.lookup(ref)
345+
evaluated_keys.extend(
346+
find_evaluated_property_keys_by_schema(
347+
validator.evolve(
348+
schema=resolved.contents,
349+
_resolver=resolved.resolver,
350+
),
351+
instance,
352+
resolved.contents,
353+
),
354+
)
355+
356+
if "$recursiveRef" in schema:
357+
resolved = lookup_recursive_ref(validator._resolver)
358+
evaluated_keys.extend(
359+
find_evaluated_property_keys_by_schema(
360+
validator.evolve(
361+
schema=resolved.contents,
362+
_resolver=resolved.resolver,
363+
),
364+
instance,
365+
resolved.contents,
366+
),
367+
)
368+
369+
for keyword in [
370+
"properties", "additionalProperties", "unevaluatedProperties",
371+
]:
372+
if keyword in schema:
373+
schema_value = schema[keyword]
374+
if validator.is_type(schema_value, "boolean") and schema_value:
375+
evaluated_keys += instance.keys()
376+
377+
elif validator.is_type(schema_value, "object"):
378+
for property in schema_value:
379+
if property in instance:
380+
evaluated_keys.append(property)
381+
382+
if "patternProperties" in schema:
383+
for property in instance:
384+
for pattern in schema["patternProperties"]:
385+
if re.search(pattern, property):
386+
evaluated_keys.append(property)
387+
388+
if "dependentSchemas" in schema:
389+
for property, subschema in schema["dependentSchemas"].items():
390+
if property not in instance:
391+
continue
392+
evaluated_keys += find_evaluated_property_keys_by_schema(
393+
validator, instance, subschema,
394+
)
395+
396+
for keyword in ["allOf", "oneOf", "anyOf"]:
397+
if keyword in schema:
398+
for subschema in schema[keyword]:
399+
errs = next(validator.descend(instance, subschema), None)
400+
if errs is None:
401+
evaluated_keys += find_evaluated_property_keys_by_schema(
402+
validator, instance, subschema,
403+
)
404+
405+
if "if" in schema:
406+
if validator.evolve(schema=schema["if"]).is_valid(instance):
407+
evaluated_keys += find_evaluated_property_keys_by_schema(
408+
validator, instance, schema["if"],
409+
)
410+
if "then" in schema:
411+
evaluated_keys += find_evaluated_property_keys_by_schema(
412+
validator, instance, schema["then"],
413+
)
414+
else:
415+
if "else" in schema:
416+
evaluated_keys += find_evaluated_property_keys_by_schema(
417+
validator, instance, schema["else"],
418+
)
419+
420+
return evaluated_keys
421+
422+
423+
def unevaluatedProperties_draft2019(validator, uP, instance, schema):
424+
if not validator.is_type(instance, "object"):
425+
return
426+
evaluated_keys = find_evaluated_property_keys_by_schema(
427+
validator, instance, schema,
428+
)
429+
unevaluated_keys = []
430+
for property in instance:
431+
if property not in evaluated_keys:
432+
for _ in validator.descend(
433+
instance[property],
434+
uP,
435+
path=property,
436+
schema_path=property,
437+
):
438+
# FIXME: Include context for each unevaluated property
439+
# indicating why it's invalid under the subschema.
440+
unevaluated_keys.append(property)
441+
442+
if unevaluated_keys:
443+
if uP is False:
444+
error = "Unevaluated properties are not allowed (%s %s unexpected)"
445+
extras = sorted(unevaluated_keys, key=str)
446+
yield ValidationError(error % _utils.extras_msg(extras))
447+
else:
448+
error = (
449+
"Unevaluated properties are not valid under "
450+
"the given schema (%s %s unevaluated and invalid)"
451+
)
452+
yield ValidationError(error % _utils.extras_msg(unevaluated_keys))

jsonschema/_utils.py

+34-4
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,23 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
197197
if "items" in schema:
198198
return list(range(0, len(instance)))
199199

200-
if "$ref" in schema:
201-
resolved = validator._resolver.lookup(schema["$ref"])
200+
ref = schema.get("$ref")
201+
if ref is not None:
202+
resolved = validator._resolver.lookup(ref)
203+
evaluated_indexes.extend(
204+
find_evaluated_item_indexes_by_schema(
205+
validator.evolve(
206+
schema=resolved.contents,
207+
_resolver=resolved.resolver,
208+
),
209+
instance,
210+
resolved.contents,
211+
),
212+
)
213+
214+
dynamicRef = schema.get("$dynamicRef")
215+
if dynamicRef is not None:
216+
resolved = validator._resolver.lookup(dynamicRef)
202217
evaluated_indexes.extend(
203218
find_evaluated_item_indexes_by_schema(
204219
validator.evolve(
@@ -258,8 +273,23 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
258273
return []
259274
evaluated_keys = []
260275

261-
if "$ref" in schema:
262-
resolved = validator._resolver.lookup(schema["$ref"])
276+
ref = schema.get("$ref")
277+
if ref is not None:
278+
resolved = validator._resolver.lookup(ref)
279+
evaluated_keys.extend(
280+
find_evaluated_property_keys_by_schema(
281+
validator.evolve(
282+
schema=resolved.contents,
283+
_resolver=resolved.resolver,
284+
),
285+
instance,
286+
resolved.contents,
287+
),
288+
)
289+
290+
dynamicRef = schema.get("$dynamicRef")
291+
if dynamicRef is not None:
292+
resolved = validator._resolver.lookup(dynamicRef)
263293
evaluated_keys.extend(
264294
find_evaluated_property_keys_by_schema(
265295
validator.evolve(

jsonschema/validators.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,9 @@ def extend(
782782
"required": _keywords.required,
783783
"type": _keywords.type,
784784
"unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019,
785-
"unevaluatedProperties": _keywords.unevaluatedProperties,
785+
"unevaluatedProperties": (
786+
_legacy_keywords.unevaluatedProperties_draft2019
787+
),
786788
"uniqueItems": _keywords.uniqueItems,
787789
},
788790
type_checker=_types.draft201909_type_checker,

0 commit comments

Comments
 (0)