33from collections .abc import Iterator
44from collections .abc import Mapping
55from collections .abc import Sequence
6+ from functools import partial
67from typing import TYPE_CHECKING
78from typing import Any
89from typing import cast
1112from jsonschema .exceptions import SchemaError
1213from jsonschema .exceptions import ValidationError
1314from jsonschema .protocols import Validator
15+ from jsonschema .validators import validator_for
1416from jsonschema_path .paths import SchemaPath
1517from openapi_schema_validator import oas30_format_checker
1618from openapi_schema_validator import oas31_format_checker
1719from openapi_schema_validator .validators import OAS30Validator
1820from openapi_schema_validator .validators import OAS31Validator
21+ from openapi_schema_validator .validators import check_openapi_schema
1922
2023from openapi_spec_validator .validation .exceptions import (
2124 DuplicateOperationIDError ,
3437 KeywordValidatorRegistry ,
3538 )
3639
40+ OAS31_BASE_DIALECT_URI = "https://spec.openapis.org/oas/3.1/dialect/base"
41+
3742
3843class KeywordValidator :
3944 def __init__ (self , registry : "KeywordValidatorRegistry" ):
@@ -101,6 +106,32 @@ def _collect_properties(self, schema: SchemaPath) -> set[str]:
101106
102107 return props
103108
109+ def _get_schema_checker (
110+ self , schema : SchemaPath , schema_value : Any
111+ ) -> Callable [[Any ], None ]:
112+ return cast (
113+ Callable [[Any ], None ],
114+ getattr (
115+ self .default_validator .value_validator_cls ,
116+ "check_schema" ,
117+ ),
118+ )
119+
120+ def _validate_schema_meta (
121+ self , schema : SchemaPath , schema_value : Any
122+ ) -> OpenAPIValidationError | None :
123+ try :
124+ schema_checker = self ._get_schema_checker (schema , schema_value )
125+ except ValueError as exc :
126+ return OpenAPIValidationError (str (exc ))
127+ try :
128+ schema_checker (schema_value )
129+ except (SchemaError , ValidationError ) as err :
130+ return cast (
131+ OpenAPIValidationError , OpenAPIValidationError .create_from (err )
132+ )
133+ return None
134+
104135 def __call__ (
105136 self ,
106137 schema : SchemaPath ,
@@ -114,23 +145,17 @@ def __call__(
114145 )
115146 return
116147
148+ schema_id = id (schema_value )
117149 if not meta_checked :
118150 assert self .meta_checked_schema_ids is not None
119- schema_id = id (schema_value )
120151 if schema_id not in self .meta_checked_schema_ids :
121- try :
122- schema_check = getattr (
123- self .default_validator .value_validator_cls ,
124- "check_schema" ,
125- )
126- schema_check (schema_value )
127- except (SchemaError , ValidationError ) as err :
128- yield OpenAPIValidationError .create_from (err )
129- return
130152 self .meta_checked_schema_ids .append (schema_id )
153+ err = self ._validate_schema_meta (schema , schema_value )
154+ if err is not None :
155+ yield err
156+ return
131157
132158 assert self .visited_schema_ids is not None
133- schema_id = id (schema_value )
134159 if schema_id in self .visited_schema_ids :
135160 return
136161 self .visited_schema_ids .append (schema_id )
@@ -218,6 +243,54 @@ def __call__(
218243 yield from self .default_validator (schema , default_value )
219244
220245
246+ class OpenAPIV31SchemaValidator (SchemaValidator ):
247+ default_jsonschema_dialect_id = OAS31_BASE_DIALECT_URI
248+
249+ def _get_schema_checker (
250+ self , schema : SchemaPath , schema_value : Any
251+ ) -> Callable [[Any ], None ]:
252+ if isinstance (schema_value , Mapping ):
253+ schema_to_check = dict (schema_value )
254+ if "$schema" in schema_to_check :
255+ dialect_source = schema_to_check
256+ else :
257+ jsonschema_dialect_id = self ._get_jsonschema_dialect_id (schema )
258+ dialect_source = {"$schema" : jsonschema_dialect_id }
259+ schema_to_check = {
260+ ** schema_to_check ,
261+ "$schema" : jsonschema_dialect_id ,
262+ }
263+ else :
264+ jsonschema_dialect_id = self ._get_jsonschema_dialect_id (schema )
265+ dialect_source = {"$schema" : jsonschema_dialect_id }
266+ schema_to_check = schema_value
267+
268+ validator_cls = validator_for (
269+ dialect_source ,
270+ default = cast (Any , None ),
271+ )
272+ if validator_cls is None :
273+ raise ValueError (
274+ f"Unknown JSON Schema dialect: { dialect_source ['$schema' ]!r} "
275+ )
276+ return partial (
277+ check_openapi_schema ,
278+ validator_cls ,
279+ format_checker = oas31_format_checker ,
280+ )
281+
282+ def _get_jsonschema_dialect_id (self , schema : SchemaPath ) -> str :
283+ schema_root = self ._get_schema_root (schema )
284+ try :
285+ return (schema_root // "jsonSchemaDialect" ).read_str ()
286+ except KeyError :
287+ return self .default_jsonschema_dialect_id
288+
289+ def _get_schema_root (self , schema : SchemaPath ) -> SchemaPath :
290+ # jsonschema-path currently has no public API for root traversal.
291+ return schema ._clone_with_parts (())
292+
293+
221294class SchemasValidator (KeywordValidator ):
222295 @property
223296 def schema_validator (self ) -> SchemaValidator :
0 commit comments