Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ result = openapi.unmarshal_response(request, response)

By default, OpenAPI follows JSON Schema behavior: when an object schema omits `additionalProperties`, extra keys are allowed.

If you want stricter behavior, enable `strict_additional_properties`. In this mode, omitted `additionalProperties` is treated as `false`.
If you want stricter behavior, change `additional_properties_default_policy` to `forbid`. In this mode, omitted `additionalProperties` is treated as `false`.

This mode is particularly useful for:
- **Preventing data leaks**: Ensuring your API doesn't accidentally expose internal or sensitive fields in responses that aren't explicitly documented.
Expand All @@ -148,7 +148,7 @@ from openapi_core import Config
from openapi_core import OpenAPI

config = Config(
strict_additional_properties=True,
additional_properties_default_policy="forbid",
)
openapi = OpenAPI.from_file_path('openapi.json', config=config)
```
Expand Down
24 changes: 16 additions & 8 deletions openapi_core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ def request_validator(self) -> RequestValidator:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
security_provider_factory=self.config.security_provider_factory,
)

Expand All @@ -446,7 +447,8 @@ def response_validator(self) -> ResponseValidator:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
enforce_properties_required=self.config.response_properties_default_policy
== "required",
)
Expand All @@ -466,7 +468,8 @@ def webhook_request_validator(self) -> WebhookRequestValidator:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
security_provider_factory=self.config.security_provider_factory,
)

Expand All @@ -485,7 +488,8 @@ def webhook_response_validator(self) -> WebhookResponseValidator:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
enforce_properties_required=self.config.response_properties_default_policy
== "required",
)
Expand All @@ -505,7 +509,8 @@ def request_unmarshaller(self) -> RequestUnmarshaller:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
security_provider_factory=self.config.security_provider_factory,
schema_unmarshallers_factory=self.config.schema_unmarshallers_factory,
extra_format_unmarshallers=self.config.extra_format_unmarshallers,
Expand All @@ -526,7 +531,8 @@ def response_unmarshaller(self) -> ResponseUnmarshaller:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
enforce_properties_required=self.config.response_properties_default_policy
== "required",
schema_unmarshallers_factory=self.config.schema_unmarshallers_factory,
Expand All @@ -548,7 +554,8 @@ def webhook_request_unmarshaller(self) -> WebhookRequestUnmarshaller:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
security_provider_factory=self.config.security_provider_factory,
schema_unmarshallers_factory=self.config.schema_unmarshallers_factory,
extra_format_unmarshallers=self.config.extra_format_unmarshallers,
Expand All @@ -569,7 +576,8 @@ def webhook_response_unmarshaller(self) -> WebhookResponseUnmarshaller:
spec_validator_cls=self.config.spec_validator_cls,
extra_format_validators=self.config.extra_format_validators,
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
strict_additional_properties=self.config.strict_additional_properties,
forbid_unspecified_additional_properties=self.config.additional_properties_default_policy
== "forbid",
enforce_properties_required=self.config.response_properties_default_policy
== "required",
schema_unmarshallers_factory=self.config.schema_unmarshallers_factory,
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/unmarshalling/request/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(
MediaTypeDeserializersDict
] = None,
security_provider_factory: SecurityProviderFactory = security_provider_factory,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
schema_unmarshallers_factory: Optional[
SchemaUnmarshallersFactory
] = None,
Expand Down Expand Up @@ -91,7 +91,7 @@ def __init__(
MediaTypeDeserializersDict
] = None,
security_provider_factory: SecurityProviderFactory = security_provider_factory,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
schema_unmarshallers_factory: Optional[
SchemaUnmarshallersFactory
] = None,
Expand Down
6 changes: 3 additions & 3 deletions openapi_core/unmarshalling/request/unmarshallers.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def __init__(
MediaTypeDeserializersDict
] = None,
security_provider_factory: SecurityProviderFactory = security_provider_factory,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
schema_unmarshallers_factory: Optional[
SchemaUnmarshallersFactory
] = None,
Expand All @@ -140,7 +140,7 @@ def __init__(
format_validators=format_validators,
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
strict_additional_properties=strict_additional_properties,
forbid_unspecified_additional_properties=forbid_unspecified_additional_properties,
schema_unmarshallers_factory=schema_unmarshallers_factory,
format_unmarshallers=format_unmarshallers,
extra_format_unmarshallers=extra_format_unmarshallers,
Expand All @@ -159,7 +159,7 @@ def __init__(
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
security_provider_factory=security_provider_factory,
strict_additional_properties=strict_additional_properties,
forbid_unspecified_additional_properties=forbid_unspecified_additional_properties,
)

def _unmarshal(
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/unmarshalling/response/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(
extra_media_type_deserializers: Optional[
MediaTypeDeserializersDict
] = None,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
enforce_properties_required: bool = False,
schema_unmarshallers_factory: Optional[
SchemaUnmarshallersFactory
Expand Down Expand Up @@ -92,7 +92,7 @@ def __init__(
extra_media_type_deserializers: Optional[
MediaTypeDeserializersDict
] = None,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
enforce_properties_required: bool = False,
schema_unmarshallers_factory: Optional[
SchemaUnmarshallersFactory
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/unmarshalling/schemas/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def create(
format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
extra_format_validators: Optional[FormatValidatorsDict] = None,
extra_format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
enforce_properties_required: bool = False,
) -> SchemaUnmarshaller:
"""Create unmarshaller from the schema."""
Expand All @@ -54,7 +54,7 @@ def create(
schema,
format_validators=format_validators,
extra_format_validators=extra_format_validators,
strict_additional_properties=strict_additional_properties,
forbid_unspecified_additional_properties=forbid_unspecified_additional_properties,
enforce_properties_required=enforce_properties_required,
)

Expand Down
6 changes: 3 additions & 3 deletions openapi_core/unmarshalling/unmarshallers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(
extra_media_type_deserializers: Optional[
MediaTypeDeserializersDict
] = None,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
enforce_properties_required: bool = False,
schema_unmarshallers_factory: Optional[
SchemaUnmarshallersFactory
Expand All @@ -75,7 +75,7 @@ def __init__(
format_validators=format_validators,
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
strict_additional_properties=strict_additional_properties,
forbid_unspecified_additional_properties=forbid_unspecified_additional_properties,
enforce_properties_required=enforce_properties_required,
)
self.schema_unmarshallers_factory = (
Expand All @@ -93,7 +93,7 @@ def _unmarshal_schema(self, schema: SchemaPath, value: Any) -> Any:
schema,
format_validators=self.format_validators,
extra_format_validators=self.extra_format_validators,
strict_additional_properties=self.strict_additional_properties,
forbid_unspecified_additional_properties=self.forbid_unspecified_additional_properties,
enforce_properties_required=self.enforce_properties_required,
format_unmarshallers=self.format_unmarshallers,
extra_format_unmarshallers=self.extra_format_unmarshallers,
Expand Down
6 changes: 3 additions & 3 deletions openapi_core/validation/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class ValidatorConfig:
Extra media type deserializers.
security_provider_factory
Security providers factory.
strict_additional_properties
If true, treat schemas that omit additionalProperties as if
additional_properties_default_policy
If forbid, treat schemas that omit additionalProperties as if
additionalProperties: false.
response_properties_default_policy
If true, response schema properties are treated as required during
Expand All @@ -70,7 +70,7 @@ class ValidatorConfig:
security_provider_factory: SecurityProviderFactory = (
security_provider_factory
)
strict_additional_properties: bool = False
additional_properties_default_policy: Literal["allow", "forbid"] = "allow"
response_properties_default_policy: Literal["optional", "required"] = (
"optional"
)
4 changes: 2 additions & 2 deletions openapi_core/validation/request/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(
MediaTypeDeserializersDict
] = None,
security_provider_factory: SecurityProviderFactory = security_provider_factory,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
): ...

def iter_errors(
Expand Down Expand Up @@ -85,7 +85,7 @@ def __init__(
MediaTypeDeserializersDict
] = None,
security_provider_factory: SecurityProviderFactory = security_provider_factory,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
): ...

def iter_errors(
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/validation/request/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def __init__(
MediaTypeDeserializersDict
] = None,
security_provider_factory: SecurityProviderFactory = security_provider_factory,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
):

BaseValidator.__init__(
Expand All @@ -101,7 +101,7 @@ def __init__(
format_validators=format_validators,
extra_format_validators=extra_format_validators,
extra_media_type_deserializers=extra_media_type_deserializers,
strict_additional_properties=strict_additional_properties,
forbid_unspecified_additional_properties=forbid_unspecified_additional_properties,
)
self.security_provider_factory = security_provider_factory

Expand Down
4 changes: 2 additions & 2 deletions openapi_core/validation/response/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(
extra_media_type_deserializers: Optional[
MediaTypeDeserializersDict
] = None,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
enforce_properties_required: bool = False,
): ...

Expand Down Expand Up @@ -85,7 +85,7 @@ def __init__(
extra_media_type_deserializers: Optional[
MediaTypeDeserializersDict
] = None,
strict_additional_properties: bool = False,
forbid_unspecified_additional_properties: bool = False,
enforce_properties_required: bool = False,
): ...

Expand Down
18 changes: 13 additions & 5 deletions openapi_core/validation/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from openapi_schema_validator import OAS32Validator

from openapi_core.validation.schemas._validators import (
build_strict_additional_properties_validator,
build_forbid_unspecified_additional_properties_validator,
)
from openapi_core.validation.schemas.factories import SchemaValidatorsFactory

Expand All @@ -22,7 +22,8 @@
OAS30WriteValidator,
Proxy(
partial(
build_strict_additional_properties_validator, OAS30WriteValidator
build_forbid_unspecified_additional_properties_validator,
OAS30WriteValidator,
)
),
)
Expand All @@ -31,15 +32,19 @@
OAS30ReadValidator,
Proxy(
partial(
build_strict_additional_properties_validator, OAS30ReadValidator
build_forbid_unspecified_additional_properties_validator,
OAS30ReadValidator,
)
),
)

oas31_schema_validators_factory = SchemaValidatorsFactory(
OAS31Validator,
Proxy(
partial(build_strict_additional_properties_validator, OAS31Validator)
partial(
build_forbid_unspecified_additional_properties_validator,
OAS31Validator,
)
),
# NOTE: Intentionally use OAS 3.0 format checker for OAS 3.1 to preserve
# backward compatibility for `byte`/`binary` formats.
Expand All @@ -50,6 +55,9 @@
oas32_schema_validators_factory = SchemaValidatorsFactory(
OAS32Validator,
Proxy(
partial(build_strict_additional_properties_validator, OAS32Validator)
partial(
build_forbid_unspecified_additional_properties_validator,
OAS32Validator,
)
),
)
41 changes: 40 additions & 1 deletion openapi_core/validation/schemas/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from jsonschema.validators import extend


def build_strict_additional_properties_validator(
def build_forbid_unspecified_additional_properties_validator(
validator_class: type[Validator],
) -> type[Validator]:
properties_validator = validator_class.VALIDATORS.get("properties")
Expand Down Expand Up @@ -81,3 +81,42 @@ def iter_missing_additional_properties_errors(
if extras:
error = "Additional properties are not allowed (%s %s unexpected)"
yield ValidationError(error % extras_msg(extras))


def build_enforce_properties_required_validator(
validator_class: type[Validator],
) -> type[Validator]:
properties_validator = validator_class.VALIDATORS.get("properties")

def enforce_properties(
validator: Any,
properties: Any,
instance: Any,
schema: Mapping[str, Any],
) -> Iterator[Any]:
if properties_validator is not None:
yield from properties_validator(
validator, properties, instance, schema
)

if not validator.is_type(instance, "object"):
return

for prop_name, prop_schema in properties.items():
if prop_name not in instance:
if (
isinstance(prop_schema, dict)
and prop_schema.get("writeOnly") is True
):
continue
yield ValidationError(f"'{prop_name}' is a required property")

return cast(
type[Validator],
extend(
validator_class,
validators={
"properties": enforce_properties,
},
),
)
Loading
Loading