Skip to content

Commit 93b17bb

Browse files
committed
Add opt-in OpenAPI 3.0 Schema Object metaschema check
OpenAPI 3.0 schemas are checked against the JSON Schema Draft 4 metaschema, which accepts keywords OpenAPI 3.0 does not support (type arrays, definitions, id, $schema, patternProperties, const, ...). Vendor the official OAS 3.0 schema (2024-10-18) unmodified and add metaschema="openapi" to OAS 3.0 check_schema, plus oas30_metaschema on the validate shortcut. The check uses the Schema, Reference, Discriminator, XML and ExternalDocumentation definitions, choosing Schema or Reference by the presence of $ref instead of oneOf so errors point at the offending keyword. Local reference targets are checked against the same metaschema, including targets declaring $schema. The default stays "draft4"; the cache key includes the selected metaschema.
1 parent 4bdf8a4 commit 93b17bb

8 files changed

Lines changed: 2109 additions & 15 deletions

File tree

‎docs/validation.rst‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,37 @@ dialect either. Schema checks still follow local JSON pointer references
168168
(``#/...``) and check the schemas they point to, wherever those are stored in
169169
the document.
170170

171+
OpenAPI 3.0 Schema Object metaschema
172+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
173+
174+
By default OpenAPI 3.0 schemas are checked against the JSON Schema Draft 4
175+
metaschema, which accepts keywords that OpenAPI 3.0 does not support, such as
176+
``type`` arrays, ``definitions``, ``id``, ``$schema``, ``patternProperties``
177+
or ``const``. Pass ``metaschema="openapi"`` to ``check_schema`` (or
178+
``oas30_metaschema="openapi"`` to ``validate``) to check against the
179+
`official OpenAPI 3.0 Schema Object schema
180+
<https://spec.openapis.org/oas/3.0/schema/2024-10-18>`__ instead. It allows
181+
only the fields listed by the specification and ``x-`` extensions:
182+
183+
.. code-block:: python
184+
185+
from openapi_schema_validator import OAS30Validator
186+
from openapi_schema_validator import validate
187+
188+
schema = {"type": ["string", "null"]}
189+
190+
OAS30Validator.check_schema(schema) # passes with the Draft 4 metaschema
191+
192+
OAS30Validator.check_schema(schema, metaschema="openapi")
193+
# SchemaError: ['string', 'null'] is not of type 'string'
194+
195+
validate("John", schema, cls=OAS30Validator, oas30_metaschema="openapi")
196+
# SchemaError: ['string', 'null'] is not of type 'string'
197+
198+
The ``openapi`` metaschema is opt-in for now and is planned to become the
199+
default for OpenAPI 3.0 in a future major release. ``oas30_metaschema`` is
200+
accepted only with OpenAPI 3.0 validator classes.
201+
171202
Default dialect resolution
172203
--------------------------
173204

‎openapi_schema_validator/_caches.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,14 @@ def build_key(
5454
args: tuple[Any, ...],
5555
kwargs: Mapping[str, Any],
5656
allow_remote_references: bool,
57+
schema_check_options: Hashable = (),
5758
) -> Hashable:
59+
# ``schema_checked`` is only meaningful for the options it was
60+
# checked with, so they are part of the key.
5861
return (
5962
cls,
6063
allow_remote_references,
64+
schema_check_options,
6165
self._schema_fingerprint(schema),
6266
self._freeze_value(args),
6367
self._freeze_value(dict(kwargs)),

‎openapi_schema_validator/_dialects.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
)
88

99
__all__ = [
10+
"OAS30_SCHEMA_ID",
1011
"OAS31_BASE_DIALECT_ID",
1112
"OAS31_BASE_DIALECT_METASCHEMA",
1213
"OAS32_BASE_DIALECT_ID",
1314
"OAS32_BASE_DIALECT_METASCHEMA",
1415
"register_openapi_dialect",
1516
]
1617

18+
#: Official JSON Schema for OpenAPI 3.0 documents. Its ``Schema`` and
19+
#: ``Reference`` definitions describe the OpenAPI 3.0 Schema Object.
20+
OAS30_SCHEMA_ID = "https://spec.openapis.org/oas/3.0/schema/2024-10-18"
1721
OAS31_BASE_DIALECT_ID = "https://spec.openapis.org/oas/3.1/dialect/base"
1822
OAS31_BASE_DIALECT_METASCHEMA = OPENAPI_SPECIFICATIONS.contents(
1923
OAS31_BASE_DIALECT_ID,

‎openapi_schema_validator/_references.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,15 @@ def _resolve_pointer(
106106

107107
def iter_unchecked_reference_targets(
108108
schema: Mapping[str, Any],
109+
*,
110+
skip_other_dialects: bool = True,
109111
) -> Iterator[Mapping[str, Any]]:
110112
"""Yield reachable local reference targets the structural check missed.
111113
112114
Each target is yielded once. Targets inside a yielded target are
113-
treated as covered by that target's check.
115+
treated as covered by that target's check. With ``skip_other_dialects``
116+
(the default), schemas declaring another dialect via ``$schema`` are
117+
left to that dialect.
114118
"""
115119
covered: set[int] = set()
116120
_mark_covered(schema, covered)
@@ -125,7 +129,11 @@ def iter_unchecked_reference_targets(
125129
visited.add(key)
126130

127131
if "$ref" not in current:
128-
if current is not schema and declares_other_dialect(current):
132+
if (
133+
skip_other_dialects
134+
and current is not schema
135+
and declares_other_dialect(current)
136+
):
129137
continue
130138
# Definitions become reachable through references only.
131139
for child in _children(current, include_definitions=False):
@@ -142,7 +150,9 @@ def iter_unchecked_reference_targets(
142150
except (LookupError, TypeError, ValueError):
143151
# Unresolvable references are reported during validation.
144152
continue
145-
if not isinstance(target, Mapping) or declares_other_dialect(target):
153+
if not isinstance(target, Mapping):
154+
continue
155+
if skip_other_dialects and declares_other_dialect(target):
146156
continue
147157
if id(target) not in covered:
148158
_mark_covered(target, covered)

0 commit comments

Comments
 (0)