Skip to content

Commit 11113dd

Browse files
committed
Enforce properties required flag
1 parent 0508964 commit 11113dd

5 files changed

Lines changed: 103 additions & 0 deletions

File tree

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Usage
6060
cls=OAS32Validator,
6161
allow_remote_references=False,
6262
check_schema=True,
63+
enforce_properties_required=False,
6364
**kwargs,
6465
)
6566
@@ -94,6 +95,12 @@ accept jsonschema's default remote retrieval behavior.
9495
validating an instance. For trusted pre-validated schemas in hot paths, set
9596
``check_schema=False`` to skip schema checking.
9697

98+
When ``enforce_properties_required=True`` is passed, all properties declared
99+
in the schema's ``properties`` object are strictly required to be present in
100+
the instance (except those marked as ``writeOnly`` or ``readOnly`` where
101+
appropriate), regardless of the schema's ``required`` array. This is useful for
102+
response or contract testing to ensure no documented fields are missing.
103+
97104
The ``validate`` helper keeps an internal compiled-validator cache. You can
98105
control cache size using the
99106
``OPENAPI_SCHEMA_VALIDATOR_COMPILED_VALIDATOR_CACHE_MAX_SIZE`` environment variable

docs/validation.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Validate
1616
cls=OAS32Validator,
1717
allow_remote_references=False,
1818
check_schema=True,
19+
enforce_properties_required=False,
1920
**kwargs,
2021
)
2122
@@ -39,6 +40,12 @@ jsonschema's default remote retrieval behavior.
3940
For trusted pre-validated schemas in hot paths, set ``check_schema=False`` to
4041
skip schema checking.
4142

43+
When ``enforce_properties_required=True`` is passed, all properties declared
44+
in the schema's ``properties`` object are strictly required to be present in
45+
the instance (except those marked as ``writeOnly`` or ``readOnly`` where
46+
appropriate), regardless of the schema's ``required`` array. This is useful for
47+
response or contract testing to ensure no documented fields are missing.
48+
4249
The shortcut keeps an internal compiled-validator cache.
4350
Use ``OPENAPI_SCHEMA_VALIDATOR_COMPILED_VALIDATOR_CACHE_MAX_SIZE`` to control cache
4451
capacity (default: ``128``).

openapi_schema_validator/shortcuts.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_ID
1313
from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID
1414
from openapi_schema_validator.validators import OAS32Validator
15+
from openapi_schema_validator.validators import (
16+
build_enforce_properties_required_validator,
17+
)
1518
from openapi_schema_validator.validators import check_openapi_schema
1619

1720
_LOCAL_ONLY_REGISTRY = Registry()
@@ -42,6 +45,7 @@ def validate(
4245
*args: Any,
4346
allow_remote_references: bool = False,
4447
check_schema: bool = True,
48+
enforce_properties_required: bool = False,
4549
**kwargs: Any,
4650
) -> None:
4751
"""
@@ -65,6 +69,11 @@ def validate(
6569
check_schema: If ``True`` (default), validate the provided schema
6670
before validating ``instance``. If ``False``, skip schema
6771
validation and run instance validation directly.
72+
enforce_properties_required: If ``True``, all properties declared in
73+
the schema's ``properties`` object are strictly required to be
74+
present in the instance (except those marked as ``writeOnly`` or
75+
``readOnly`` where appropriate), regardless of the schema's
76+
``required`` array. Defaults to ``False``.
6877
**kwargs: Keyword arguments forwarded to ``cls`` constructor
6978
(for example ``registry`` and ``format_checker``). If omitted,
7079
a local-only empty ``Registry`` is used to avoid implicit remote
@@ -74,6 +83,9 @@ def validate(
7483
jsonschema.exceptions.SchemaError: If ``schema`` is invalid.
7584
jsonschema.exceptions.ValidationError: If ``instance`` is invalid.
7685
"""
86+
if enforce_properties_required:
87+
cls = build_enforce_properties_required_validator(cls) # type: ignore[arg-type]
88+
7789
schema_dict = cast(dict[str, Any], schema)
7890

7991
validator_kwargs = kwargs.copy()

openapi_schema_validator/validators.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from functools import lru_cache
12
from typing import Any
3+
from typing import Iterator
4+
from typing import Mapping
25
from typing import cast
36

47
from jsonschema import _keywords
58
from jsonschema import _legacy_keywords
69
from jsonschema.exceptions import SchemaError
710
from jsonschema.exceptions import ValidationError
11+
from jsonschema.protocols import Validator
812
from jsonschema.validators import Draft202012Validator
913
from jsonschema.validators import create
1014
from jsonschema.validators import extend
@@ -187,3 +191,47 @@ def _build_oas32_validator() -> Any:
187191
OAS30Validator.check_schema = classmethod(check_openapi_schema)
188192
OAS31Validator.check_schema = classmethod(check_openapi_schema)
189193
OAS32Validator.check_schema = classmethod(check_openapi_schema)
194+
195+
196+
@lru_cache(maxsize=None)
197+
def build_enforce_properties_required_validator(
198+
validator_class: Any,
199+
) -> type[Validator]:
200+
properties_validator = validator_class.VALIDATORS.get("properties")
201+
required_validator = validator_class.VALIDATORS.get("required")
202+
203+
def enforce_properties(
204+
validator: Any,
205+
properties: Any,
206+
instance: Any,
207+
schema: Mapping[str, Any],
208+
) -> Iterator[Any]:
209+
if properties_validator is not None:
210+
yield from properties_validator(
211+
validator, properties, instance, schema
212+
)
213+
214+
if not validator.is_type(instance, "object"):
215+
return
216+
217+
if required_validator is not None:
218+
schema_required = (
219+
schema.get("required", []) if isinstance(schema, dict) else []
220+
)
221+
missing_props = [
222+
p for p in properties.keys() if p not in schema_required
223+
]
224+
if missing_props:
225+
yield from required_validator(
226+
validator, missing_props, instance, schema
227+
)
228+
229+
extended_validator = extend(
230+
validator_class,
231+
validators={"properties": enforce_properties},
232+
)
233+
if hasattr(validator_class, "check_schema"):
234+
extended_validator.check_schema = classmethod(
235+
validator_class.check_schema.__func__
236+
)
237+
return cast(type[Validator], extended_validator)

tests/unit/test_shortcut.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,32 @@ def test_validate_cache_max_size_from_env(monkeypatch):
195195
validate("foo", schema_a, cls=OAS32Validator)
196196

197197
assert check_schema_mock.call_count == 3
198+
199+
200+
from openapi_schema_validator.validators import OAS30Validator
201+
202+
203+
def test_enforce_properties_required():
204+
schema = {
205+
"type": "object",
206+
"properties": {
207+
"id": {"type": "string"},
208+
"nickname": {"type": "string"},
209+
},
210+
"required": ["id"],
211+
}
212+
instance = {"id": "42"} # nickname missing
213+
214+
# Should be valid normally
215+
validate(instance, schema, cls=OAS30Validator)
216+
217+
# Should fail with opt-in flag
218+
with pytest.raises(ValidationError) as exc:
219+
validate(
220+
instance,
221+
schema,
222+
cls=OAS30Validator,
223+
enforce_properties_required=True,
224+
)
225+
226+
assert "'nickname' is a required property" in str(exc.value)

0 commit comments

Comments
 (0)