Skip to content

Commit ba6a8af

Browse files
committed
Dialect-based schema validators
1 parent 496bcee commit ba6a8af

File tree

16 files changed

+587
-465
lines changed

16 files changed

+587
-465
lines changed

openapi_core/casting/schemas/factories.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ def __init__(
1919

2020
def create(
2121
self,
22+
spec: SchemaPath,
2223
schema: SchemaPath,
2324
format_validators: Optional[FormatValidatorsDict] = None,
2425
extra_format_validators: Optional[FormatValidatorsDict] = None,
2526
) -> SchemaCaster:
2627
schema_validator = self.schema_validators_factory.create(
28+
spec,
2729
schema,
2830
format_validators=format_validators,
2931
extra_format_validators=extra_format_validators,

openapi_core/deserializing/media_types/deserializers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def get_deserializer_callable(
6666
class MediaTypeDeserializer:
6767
def __init__(
6868
self,
69+
spec: SchemaPath,
6970
style_deserializers_factory: StyleDeserializersFactory,
7071
media_types_deserializer: MediaTypesDeserializer,
7172
mimetype: str,
@@ -75,6 +76,7 @@ def __init__(
7576
encoding: Optional[SchemaPath] = None,
7677
**parameters: str,
7778
):
79+
self.spec = spec
7880
self.style_deserializers_factory = style_deserializers_factory
7981
self.media_types_deserializer = media_types_deserializer
8082
self.mimetype = mimetype
@@ -117,6 +119,7 @@ def evolve(
117119
schema_caster = self.schema_caster.evolve(schema)
118120

119121
return cls(
122+
self.spec,
120123
self.style_deserializers_factory,
121124
self.media_types_deserializer,
122125
mimetype=mimetype or self.mimetype,
@@ -221,7 +224,7 @@ def decode_property_style(
221224
prep_encoding, default_location="query"
222225
)
223226
prop_deserializer = self.style_deserializers_factory.create(
224-
prop_style, prop_explode, prop_schema, name=prop_name
227+
self.spec, prop_schema, prop_style, prop_explode, name=prop_name
225228
)
226229
return prop_deserializer.deserialize(location)
227230

openapi_core/deserializing/media_types/factories.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def from_schema_casters_factory(
5858

5959
def create(
6060
self,
61+
spec: SchemaPath,
6162
mimetype: str,
6263
schema: Optional[SchemaPath] = None,
6364
schema_validator: Optional[SchemaValidator] = None,
@@ -89,11 +90,12 @@ def create(
8990
):
9091
schema_caster = (
9192
self.style_deserializers_factory.schema_casters_factory.create(
92-
schema
93+
spec, schema
9394
)
9495
)
9596

9697
return MediaTypeDeserializer(
98+
spec,
9799
self.style_deserializers_factory,
98100
media_types_deserializer,
99101
mimetype,

openapi_core/deserializing/styles/deserializers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ def __init__(
1717
style: str,
1818
explode: bool,
1919
name: str,
20-
schema: SchemaPath,
20+
schema_type: str,
2121
caster: SchemaCaster,
2222
deserializer_callable: Optional[DeserializerCallable] = None,
2323
):
2424
self.style = style
2525
self.explode = explode
2626
self.name = name
27-
self.schema = schema
28-
self.schema_type = (schema / "type").read_str("")
27+
self.schema_type = schema_type
2928
self.caster = caster
3029
self.deserializer_callable = deserializer_callable
3130

openapi_core/deserializing/styles/factories.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ def __init__(
2020

2121
def create(
2222
self,
23+
spec: SchemaPath,
24+
schema: SchemaPath,
2325
style: str,
2426
explode: bool,
25-
schema: SchemaPath,
2627
name: str,
2728
) -> StyleDeserializer:
2829
deserialize_callable = self.style_deserializers.get(style)
29-
caster = self.schema_casters_factory.create(schema)
30+
caster = self.schema_casters_factory.create(spec, schema)
31+
schema_type = (schema / "type").read_str("")
3032
return StyleDeserializer(
31-
style, explode, name, schema, caster, deserialize_callable
33+
style, explode, name, schema_type, caster, deserialize_callable
3234
)

openapi_core/unmarshalling/schemas/factories.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(
3333

3434
def create(
3535
self,
36+
spec: SchemaPath,
3637
schema: SchemaPath,
3738
format_validators: Optional[FormatValidatorsDict] = None,
3839
format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
@@ -51,6 +52,7 @@ def create(
5152
if extra_format_validators is None:
5253
extra_format_validators = {}
5354
schema_validator = self.schema_validators_factory.create(
55+
spec,
5456
schema,
5557
format_validators=format_validators,
5658
extra_format_validators=extra_format_validators,

openapi_core/unmarshalling/unmarshallers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(
9090

9191
def _unmarshal_schema(self, schema: SchemaPath, value: Any) -> Any:
9292
unmarshaller = self.schema_unmarshallers_factory.create(
93+
self.spec,
9394
schema,
9495
format_validators=self.format_validators,
9596
extra_format_validators=self.extra_format_validators,
Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
from functools import partial
2-
3-
from lazy_object_proxy import Proxy
1+
from openapi_schema_validator import OAS31_BASE_DIALECT_ID
2+
from openapi_schema_validator import OAS32_BASE_DIALECT_ID
43
from openapi_schema_validator import OAS30ReadValidator
54
from openapi_schema_validator import OAS30WriteValidator
65
from openapi_schema_validator import OAS31Validator
76
from openapi_schema_validator import OAS32Validator
87

9-
from openapi_core.validation.schemas._validators import (
10-
build_forbid_unspecified_additional_properties_validator,
8+
from openapi_core.validation.schemas.factories import (
9+
DialectSchemaValidatorsFactory,
1110
)
1211
from openapi_core.validation.schemas.factories import SchemaValidatorsFactory
1312

@@ -20,44 +19,22 @@
2019

2120
oas30_write_schema_validators_factory = SchemaValidatorsFactory(
2221
OAS30WriteValidator,
23-
Proxy(
24-
partial(
25-
build_forbid_unspecified_additional_properties_validator,
26-
OAS30WriteValidator,
27-
)
28-
),
2922
)
3023

3124
oas30_read_schema_validators_factory = SchemaValidatorsFactory(
3225
OAS30ReadValidator,
33-
Proxy(
34-
partial(
35-
build_forbid_unspecified_additional_properties_validator,
36-
OAS30ReadValidator,
37-
)
38-
),
3926
)
4027

41-
oas31_schema_validators_factory = SchemaValidatorsFactory(
28+
oas31_schema_validators_factory = DialectSchemaValidatorsFactory(
4229
OAS31Validator,
43-
Proxy(
44-
partial(
45-
build_forbid_unspecified_additional_properties_validator,
46-
OAS31Validator,
47-
)
48-
),
30+
OAS31_BASE_DIALECT_ID,
4931
# NOTE: Intentionally use OAS 3.0 format checker for OAS 3.1 to preserve
5032
# backward compatibility for `byte`/`binary` formats.
5133
# See https://github.com/python-openapi/openapi-core/issues/506
5234
format_checker=OAS30ReadValidator.FORMAT_CHECKER,
5335
)
5436

55-
oas32_schema_validators_factory = SchemaValidatorsFactory(
37+
oas32_schema_validators_factory = DialectSchemaValidatorsFactory(
5638
OAS32Validator,
57-
Proxy(
58-
partial(
59-
build_forbid_unspecified_additional_properties_validator,
60-
OAS32Validator,
61-
)
62-
),
39+
OAS32_BASE_DIALECT_ID,
6340
)

openapi_core/validation/schemas/factories.py

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
11
from copy import deepcopy
2+
from typing import Any
23
from typing import Optional
34
from typing import cast
45

56
from jsonschema._format import FormatChecker
67
from jsonschema.protocols import Validator
8+
from jsonschema.validators import validator_for
79
from jsonschema_path import SchemaPath
810

911
from openapi_core.validation.schemas._validators import (
1012
build_enforce_properties_required_validator,
1113
)
14+
from openapi_core.validation.schemas._validators import (
15+
build_forbid_unspecified_additional_properties_validator,
16+
)
1217
from openapi_core.validation.schemas.datatypes import FormatValidatorsDict
1318
from openapi_core.validation.schemas.validators import SchemaValidator
1419

1520

1621
class SchemaValidatorsFactory:
1722
def __init__(
1823
self,
19-
schema_validator_class: type[Validator],
20-
strict_schema_validator_class: Optional[type[Validator]] = None,
24+
schema_validator_cls: type[Validator],
2125
format_checker: Optional[FormatChecker] = None,
2226
):
23-
self.schema_validator_class = schema_validator_class
24-
self.strict_schema_validator_class = strict_schema_validator_class
27+
self.schema_validator_cls = schema_validator_cls
2528
if format_checker is None:
26-
format_checker = self.schema_validator_class.FORMAT_CHECKER
29+
format_checker = self.schema_validator_cls.FORMAT_CHECKER
2730
assert format_checker is not None
2831
self.format_checker = format_checker
2932

33+
def get_validator_cls(
34+
self, spec: SchemaPath, schema: SchemaPath
35+
) -> type[Validator]:
36+
return self.schema_validator_cls
37+
3038
def get_format_checker(
3139
self,
3240
format_validators: Optional[FormatValidatorsDict] = None,
@@ -57,34 +65,90 @@ def _add_validators(
5765

5866
def create(
5967
self,
68+
spec: SchemaPath,
6069
schema: SchemaPath,
6170
format_validators: Optional[FormatValidatorsDict] = None,
6271
extra_format_validators: Optional[FormatValidatorsDict] = None,
6372
forbid_unspecified_additional_properties: bool = False,
6473
enforce_properties_required: bool = False,
6574
) -> SchemaValidator:
66-
validator_class: type[Validator] = self.schema_validator_class
75+
validator_cls: type[Validator] = self.get_validator_cls(spec, schema)
76+
if enforce_properties_required:
77+
validator_cls = build_enforce_properties_required_validator(
78+
validator_cls
79+
)
6780
if forbid_unspecified_additional_properties:
68-
if self.strict_schema_validator_class is None:
69-
raise ValueError(
70-
"Strict additional properties validation is not supported "
71-
"by this factory."
81+
validator_cls = (
82+
build_forbid_unspecified_additional_properties_validator(
83+
validator_cls
7284
)
73-
validator_class = self.strict_schema_validator_class
74-
75-
if enforce_properties_required:
76-
validator_class = build_enforce_properties_required_validator(
77-
validator_class
7885
)
7986

8087
format_checker = self.get_format_checker(
8188
format_validators, extra_format_validators
8289
)
8390
with schema.resolve() as resolved:
84-
jsonschema_validator = validator_class(
91+
jsonschema_validator = validator_cls(
8592
resolved.contents,
8693
_resolver=resolved.resolver,
8794
format_checker=format_checker,
8895
)
8996

9097
return SchemaValidator(schema, jsonschema_validator)
98+
99+
100+
class DialectSchemaValidatorsFactory(SchemaValidatorsFactory):
101+
def __init__(
102+
self,
103+
schema_validator_cls: type[Validator],
104+
default_jsonschema_dialect_id: str,
105+
format_checker: Optional[FormatChecker] = None,
106+
):
107+
super().__init__(schema_validator_cls, format_checker)
108+
self.default_jsonschema_dialect_id = default_jsonschema_dialect_id
109+
110+
self._validator_classes_by_dialect: dict[
111+
str, type[Validator] | None
112+
] = {}
113+
114+
def get_validator_cls(
115+
self, spec: SchemaPath, schema: SchemaPath
116+
) -> type[Validator]:
117+
dialect_id = self._get_dialect_id(spec, schema)
118+
119+
validator_cls = self._get_validator_class_for_dialect(dialect_id)
120+
if validator_cls is None:
121+
raise ValueError(f"Unknown JSON Schema dialect: {dialect_id!r}")
122+
123+
return validator_cls
124+
125+
def _get_dialect_id(
126+
self,
127+
spec: SchemaPath,
128+
schema: SchemaPath,
129+
) -> str:
130+
try:
131+
return (schema / "$schema").read_str()
132+
except KeyError:
133+
return self._get_default_jsonschema_dialect_id(spec)
134+
135+
def _get_default_jsonschema_dialect_id(self, spec: SchemaPath) -> str:
136+
return (spec / "jsonSchemaDialect").read_str(
137+
default=self.default_jsonschema_dialect_id
138+
)
139+
140+
def _get_validator_class_for_dialect(
141+
self, dialect_id: str
142+
) -> type[Validator] | None:
143+
if dialect_id in self._validator_classes_by_dialect:
144+
return self._validator_classes_by_dialect[dialect_id]
145+
146+
validator_cls = cast(
147+
type[Validator] | None,
148+
validator_for(
149+
{"$schema": dialect_id},
150+
default=cast(Any, None),
151+
),
152+
)
153+
self._validator_classes_by_dialect[dialect_id] = validator_cls
154+
return validator_cls

openapi_core/validation/validators.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,15 @@ def _deserialise_media_type(
143143
schema_validator = None
144144
if schema is not None:
145145
schema_validator = self.schema_validators_factory.create(
146+
self.spec,
146147
schema,
147148
format_validators=self.format_validators,
148149
extra_format_validators=self.extra_format_validators,
149150
forbid_unspecified_additional_properties=self.forbid_unspecified_additional_properties,
150151
enforce_properties_required=self.enforce_properties_required,
151152
)
152153
deserializer = self.media_type_deserializers_factory.create(
154+
self.spec,
153155
mimetype,
154156
schema=schema,
155157
schema_validator=schema_validator,
@@ -169,12 +171,13 @@ def _deserialise_style(
169171
style, explode = get_style_and_explode(param_or_header)
170172
schema = param_or_header / "schema"
171173
deserializer = self.style_deserializers_factory.create(
172-
style, explode, schema, name=name
174+
self.spec, schema, style, explode, name=name
173175
)
174176
return deserializer.deserialize(location)
175177

176178
def _validate_schema(self, schema: SchemaPath, value: Any) -> None:
177179
validator = self.schema_validators_factory.create(
180+
self.spec,
178181
schema,
179182
format_validators=self.format_validators,
180183
extra_format_validators=self.extra_format_validators,

0 commit comments

Comments
 (0)