Skip to content

Commit f3c0d81

Browse files
committed
Enforce properties required flag
1 parent 0508964 commit f3c0d81

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

openapi_schema_validator/shortcuts.py

Lines changed: 7 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
"""
@@ -74,6 +78,9 @@ def validate(
7478
jsonschema.exceptions.SchemaError: If ``schema`` is invalid.
7579
jsonschema.exceptions.ValidationError: If ``instance`` is invalid.
7680
"""
81+
if enforce_properties_required:
82+
cls = build_enforce_properties_required_validator(cls) # type: ignore[arg-type]
83+
7784
schema_dict = cast(dict[str, Any], schema)
7885

7986
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)