Skip to content

Commit 96d51c6

Browse files
committed
Response Properties Policy
1 parent 8a31c48 commit 96d51c6

File tree

13 files changed

+57
-40
lines changed

13 files changed

+57
-40
lines changed

docs/configuration.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ By default, OpenAPI follows JSON Schema behavior: when an object schema omits `a
138138

139139
If you want stricter behavior, enable `strict_additional_properties`. In this mode, omitted `additionalProperties` is treated as `false`.
140140

141+
This mode is particularly useful for:
142+
- **Preventing data leaks**: Ensuring your API doesn't accidentally expose internal or sensitive fields in responses that aren't explicitly documented.
143+
- **Strict client validation**: Rejecting client requests that contain typos, extraneous data, or unsupported fields, forcing clients to adhere exactly to the defined schema.
144+
- **Contract tightening**: Enforcing the exact shape of objects across your API boundaries.
145+
141146
``` python hl_lines="4"
142147
from openapi_core import Config
143148
from openapi_core import OpenAPI
@@ -153,24 +158,27 @@ When strict mode is enabled:
153158
- object schema with omitted `additionalProperties` rejects unknown fields
154159
- object schema with `additionalProperties: true` still allows unknown fields
155160

156-
## Strict Response Properties
161+
## Response Properties Policy
157162

158163
By default, OpenAPI follows JSON Schema behavior for `required`: response object properties are optional unless explicitly listed in `required`.
159164

160-
If you want stricter response checks, enable `strict_response_properties`. In this mode, response object schemas are validated as if all documented properties were required, except properties marked as `writeOnly`.
165+
If you want stricter response checks, change `response_properties_default_policy` to `required`. In this mode, response object schemas are validated as if all documented properties were required (except properties marked as `writeOnly` in OpenAPI 3.0).
166+
167+
This mode is intentionally stricter than the OpenAPI default. It is particularly useful for:
168+
- **Contract completeness checks in tests**: Ensuring that the backend actually returns all the properties documented in the OpenAPI specification.
169+
- **Detecting API drift**: Catching bugs where a database schema change or serializer update inadvertently drops fields from the response.
170+
- **Preventing silent failures**: Making sure clients aren't broken by missing data that they expect to be present according to the API documentation.
161171

162172
``` python hl_lines="4"
163173
from openapi_core import Config
164174
from openapi_core import OpenAPI
165175

166176
config = Config(
167-
strict_response_properties=True,
177+
response_properties_default_policy="required",
168178
)
169179
openapi = OpenAPI.from_file_path('openapi.json', config=config)
170180
```
171181

172-
This mode is intentionally stricter than the OpenAPI default and is useful for contract completeness checks in tests.
173-
174182
## Extra Format Validators
175183

176184
OpenAPI defines a `format` keyword that hints at how a value should be interpreted. For example, a `string` with the format `date` should conform to the RFC 3339 date format.

openapi_core/app.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,8 @@ def response_validator(self) -> ResponseValidator:
447447
extra_format_validators=self.config.extra_format_validators,
448448
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
449449
strict_additional_properties=self.config.strict_additional_properties,
450-
strict_response_properties=self.config.strict_response_properties,
450+
enforce_properties_required=self.config.response_properties_default_policy
451+
== "required",
451452
)
452453

453454
@cached_property
@@ -485,7 +486,8 @@ def webhook_response_validator(self) -> WebhookResponseValidator:
485486
extra_format_validators=self.config.extra_format_validators,
486487
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
487488
strict_additional_properties=self.config.strict_additional_properties,
488-
strict_response_properties=self.config.strict_response_properties,
489+
enforce_properties_required=self.config.response_properties_default_policy
490+
== "required",
489491
)
490492

491493
@cached_property
@@ -525,7 +527,8 @@ def response_unmarshaller(self) -> ResponseUnmarshaller:
525527
extra_format_validators=self.config.extra_format_validators,
526528
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
527529
strict_additional_properties=self.config.strict_additional_properties,
528-
strict_response_properties=self.config.strict_response_properties,
530+
enforce_properties_required=self.config.response_properties_default_policy
531+
== "required",
529532
schema_unmarshallers_factory=self.config.schema_unmarshallers_factory,
530533
extra_format_unmarshallers=self.config.extra_format_unmarshallers,
531534
)
@@ -567,7 +570,8 @@ def webhook_response_unmarshaller(self) -> WebhookResponseUnmarshaller:
567570
extra_format_validators=self.config.extra_format_validators,
568571
extra_media_type_deserializers=self.config.extra_media_type_deserializers,
569572
strict_additional_properties=self.config.strict_additional_properties,
570-
strict_response_properties=self.config.strict_response_properties,
573+
enforce_properties_required=self.config.response_properties_default_policy
574+
== "required",
571575
schema_unmarshallers_factory=self.config.schema_unmarshallers_factory,
572576
extra_format_unmarshallers=self.config.extra_format_unmarshallers,
573577
)

openapi_core/configurations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class Config(UnmarshallerConfig):
3838
response_unmarshaller_cls: Response unmarshaller class.
3939
webhook_request_unmarshaller_cls: Webhook request unmarshaller class.
4040
webhook_response_unmarshaller_cls: Webhook response unmarshaller class.
41-
strict_response_properties: If true, require documented response
41+
response_properties_default_policy: If true, require documented response
4242
properties (except writeOnly properties) in response validation and
4343
unmarshalling.
4444
"""

openapi_core/unmarshalling/response/protocols.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(
5656
MediaTypeDeserializersDict
5757
] = None,
5858
strict_additional_properties: bool = False,
59-
strict_response_properties: bool = False,
59+
enforce_properties_required: bool = False,
6060
schema_unmarshallers_factory: Optional[
6161
SchemaUnmarshallersFactory
6262
] = None,
@@ -93,7 +93,7 @@ def __init__(
9393
MediaTypeDeserializersDict
9494
] = None,
9595
strict_additional_properties: bool = False,
96-
strict_response_properties: bool = False,
96+
enforce_properties_required: bool = False,
9797
schema_unmarshallers_factory: Optional[
9898
SchemaUnmarshallersFactory
9999
] = None,

openapi_core/unmarshalling/schemas/factories.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def create(
3939
extra_format_validators: Optional[FormatValidatorsDict] = None,
4040
extra_format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
4141
strict_additional_properties: bool = False,
42-
require_all_properties: bool = False,
42+
enforce_properties_required: bool = False,
4343
) -> SchemaUnmarshaller:
4444
"""Create unmarshaller from the schema."""
4545
if schema is None:
@@ -55,7 +55,7 @@ def create(
5555
format_validators=format_validators,
5656
extra_format_validators=extra_format_validators,
5757
strict_additional_properties=strict_additional_properties,
58-
require_all_properties=require_all_properties,
58+
enforce_properties_required=enforce_properties_required,
5959
)
6060

6161
schema_format = (schema / "format").read_str(None)

openapi_core/unmarshalling/unmarshallers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def __init__(
5151
MediaTypeDeserializersDict
5252
] = None,
5353
strict_additional_properties: bool = False,
54-
strict_response_properties: bool = False,
54+
enforce_properties_required: bool = False,
5555
schema_unmarshallers_factory: Optional[
5656
SchemaUnmarshallersFactory
5757
] = None,
@@ -76,7 +76,7 @@ def __init__(
7676
extra_format_validators=extra_format_validators,
7777
extra_media_type_deserializers=extra_media_type_deserializers,
7878
strict_additional_properties=strict_additional_properties,
79-
strict_response_properties=strict_response_properties,
79+
enforce_properties_required=enforce_properties_required,
8080
)
8181
self.schema_unmarshallers_factory = (
8282
schema_unmarshallers_factory or self.schema_unmarshallers_factory
@@ -94,7 +94,7 @@ def _unmarshal_schema(self, schema: SchemaPath, value: Any) -> Any:
9494
format_validators=self.format_validators,
9595
extra_format_validators=self.extra_format_validators,
9696
strict_additional_properties=self.strict_additional_properties,
97-
require_all_properties=self.strict_response_properties,
97+
enforce_properties_required=self.enforce_properties_required,
9898
format_unmarshallers=self.format_unmarshallers,
9999
extra_format_unmarshallers=self.extra_format_unmarshallers,
100100
)

openapi_core/validation/configurations.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import dataclass
2+
from typing import Literal
23
from typing import Optional
34

45
from openapi_core.casting.schemas.factories import SchemaCastersFactory
@@ -46,7 +47,7 @@ class ValidatorConfig:
4647
strict_additional_properties
4748
If true, treat schemas that omit additionalProperties as if
4849
additionalProperties: false.
49-
strict_response_properties
50+
response_properties_default_policy
5051
If true, response schema properties are treated as required during
5152
response validation/unmarshalling, except properties marked as
5253
writeOnly.
@@ -70,4 +71,6 @@ class ValidatorConfig:
7071
security_provider_factory
7172
)
7273
strict_additional_properties: bool = False
73-
strict_response_properties: bool = False
74+
response_properties_default_policy: Literal["optional", "required"] = (
75+
"optional"
76+
)

openapi_core/validation/response/protocols.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def __init__(
4848
MediaTypeDeserializersDict
4949
] = None,
5050
strict_additional_properties: bool = False,
51-
strict_response_properties: bool = False,
51+
enforce_properties_required: bool = False,
5252
): ...
5353

5454
def iter_errors(
@@ -86,7 +86,7 @@ def __init__(
8686
MediaTypeDeserializersDict
8787
] = None,
8888
strict_additional_properties: bool = False,
89-
strict_response_properties: bool = False,
89+
enforce_properties_required: bool = False,
9090
): ...
9191

9292
def iter_errors(

openapi_core/validation/schemas/factories.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def create(
5959
format_validators: Optional[FormatValidatorsDict] = None,
6060
extra_format_validators: Optional[FormatValidatorsDict] = None,
6161
strict_additional_properties: bool = False,
62-
require_all_properties: bool = False,
62+
enforce_properties_required: bool = False,
6363
) -> SchemaValidator:
6464
validator_class: type[Validator] = self.schema_validator_class
6565
if strict_additional_properties:
@@ -74,7 +74,7 @@ def create(
7474
)
7575
with schema.resolve() as resolved:
7676
schema_value = resolved.contents
77-
if require_all_properties:
77+
if enforce_properties_required:
7878
schema_value = self._build_required_properties_schema(
7979
schema_value
8080
)

openapi_core/validation/validators.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(
6565
MediaTypeDeserializersDict
6666
] = None,
6767
strict_additional_properties: bool = False,
68-
strict_response_properties: bool = False,
68+
enforce_properties_required: bool = False,
6969
):
7070
self.spec = spec
7171
self.base_url = base_url
@@ -104,7 +104,7 @@ def __init__(
104104
self.extra_format_validators = extra_format_validators
105105
self.extra_media_type_deserializers = extra_media_type_deserializers
106106
self.strict_additional_properties = strict_additional_properties
107-
self.strict_response_properties = strict_response_properties
107+
self.enforce_properties_required = enforce_properties_required
108108

109109
@cached_property
110110
def path_finder(self) -> BasePathFinder:
@@ -145,7 +145,7 @@ def _deserialise_media_type(
145145
format_validators=self.format_validators,
146146
extra_format_validators=self.extra_format_validators,
147147
strict_additional_properties=self.strict_additional_properties,
148-
require_all_properties=self.strict_response_properties,
148+
enforce_properties_required=self.enforce_properties_required,
149149
)
150150
deserializer = self.media_type_deserializers_factory.create(
151151
mimetype,
@@ -177,7 +177,7 @@ def _validate_schema(self, schema: SchemaPath, value: Any) -> None:
177177
format_validators=self.format_validators,
178178
extra_format_validators=self.extra_format_validators,
179179
strict_additional_properties=self.strict_additional_properties,
180-
require_all_properties=self.strict_response_properties,
180+
enforce_properties_required=self.enforce_properties_required,
181181
)
182182
validator.validate(value)
183183

0 commit comments

Comments
 (0)