Skip to content

Commit 29cfa5f

Browse files
committed
OAS 3.1 dialect registration and validator discovery
1 parent 5015df1 commit 29cfa5f

File tree

12 files changed

+424
-36
lines changed

12 files changed

+424
-36
lines changed

README.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,26 @@ To validate an OpenAPI v3.1 schema:
100100
101101
By default, the latest OpenAPI schema syntax is expected.
102102

103+
The OpenAPI 3.1 base dialect URI is registered for
104+
``jsonschema.validators.validator_for`` resolution.
105+
Schemas declaring
106+
``"$schema": "https://spec.openapis.org/oas/3.1/dialect/base"``
107+
resolve directly to ``OAS31Validator`` without unresolved-metaschema
108+
fallback warnings.
109+
110+
.. code-block:: python
111+
112+
from jsonschema.validators import validator_for
113+
114+
from openapi_schema_validator import OAS31Validator
115+
116+
schema = {
117+
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
118+
"type": "object",
119+
}
120+
121+
assert validator_for(schema) is OAS31Validator
122+
103123
104124
Strict vs Pragmatic Validators
105125
==============================

docs/validation.rst

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ if you want to disambiguate the expected schema version, import and use ``OAS31V
6767
6868
validate({"name": "John", "age": 23}, schema, cls=OAS31Validator)
6969
70+
The OpenAPI 3.1 base dialect URI is registered for
71+
``jsonschema.validators.validator_for`` resolution.
72+
If your schema declares
73+
``"$schema": "https://spec.openapis.org/oas/3.1/dialect/base"``,
74+
``validator_for`` resolves directly to ``OAS31Validator`` without
75+
unresolved-metaschema fallback warnings.
76+
77+
.. code-block:: python
78+
79+
from jsonschema.validators import validator_for
80+
81+
from openapi_schema_validator import OAS31Validator
82+
83+
schema = {
84+
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
85+
"type": "object",
86+
}
87+
assert validator_for(schema) is OAS31Validator
88+
7089
For OpenAPI 3.2, use ``OAS32Validator`` (behaves identically to ``OAS31Validator``, since 3.2 uses the same JSON Schema dialect).
7190

7291
In order to validate OpenAPI 3.0 schema, import and use ``OAS30Validator`` instead of ``OAS31Validator``.
@@ -193,7 +212,8 @@ Example usage:
193212

194213
.. code-block:: python
195214
196-
from openapi_schema_validator import OAS30Validator, OAS30StrictValidator
215+
from openapi_schema_validator import OAS30StrictValidator
216+
from openapi_schema_validator import OAS30Validator
197217
198218
# Pragmatic (default) - accepts bytes for binary format
199219
validator = OAS30Validator({"type": "string", "format": "binary"})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import Any
2+
3+
from jsonschema.validators import validates
4+
from jsonschema_specifications import REGISTRY as JSONSCHEMA_SPECIFICATIONS
5+
6+
from openapi_schema_validator._specifications import (
7+
REGISTRY as _OPENAPI_SPECIFICATIONS,
8+
)
9+
10+
__all__ = [
11+
"JSONSCHEMA_SPECIFICATIONS",
12+
"OAS31_BASE_DIALECT_ID",
13+
"OAS31_BASE_DIALECT_METASCHEMA",
14+
"OPENAPI_SPECIFICATIONS",
15+
"register_openapi_dialect",
16+
]
17+
18+
OPENAPI_SPECIFICATIONS = _OPENAPI_SPECIFICATIONS
19+
20+
OAS31_BASE_DIALECT_ID = "https://spec.openapis.org/oas/3.1/dialect/base"
21+
OAS31_BASE_DIALECT_METASCHEMA = OPENAPI_SPECIFICATIONS.contents(
22+
OAS31_BASE_DIALECT_ID,
23+
)
24+
25+
_REGISTERED_VALIDATORS: dict[tuple[str, str], Any] = {}
26+
27+
28+
def register_openapi_dialect(
29+
*,
30+
validator: Any,
31+
dialect_id: str,
32+
version_name: str,
33+
metaschema: Any,
34+
) -> Any:
35+
key = (dialect_id, version_name)
36+
registered_validator = _REGISTERED_VALIDATORS.get(key)
37+
38+
if registered_validator is validator:
39+
return validator
40+
if registered_validator is not None:
41+
return registered_validator
42+
43+
validator.META_SCHEMA = metaschema
44+
validator = validates(version_name)(validator)
45+
_REGISTERED_VALIDATORS[key] = validator
46+
return validator
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import json
2+
from importlib.resources import files
3+
from typing import Any
4+
from typing import Iterator
5+
6+
from jsonschema_specifications import REGISTRY as JSONSCHEMA_REGISTRY
7+
from referencing import Resource
8+
9+
__all__ = ["REGISTRY"]
10+
11+
12+
def _iter_schema_files() -> Iterator[Any]:
13+
schema_root = files(__package__).joinpath("schemas")
14+
stack = [schema_root]
15+
16+
while stack:
17+
current = stack.pop()
18+
for child in current.iterdir():
19+
if child.name.startswith("."):
20+
continue
21+
if child.is_dir():
22+
stack.append(child)
23+
continue
24+
yield child
25+
26+
27+
def _load_schemas() -> Iterator[Resource]:
28+
for path in _iter_schema_files():
29+
contents = json.loads(path.read_text(encoding="utf-8"))
30+
yield Resource.from_contents(contents)
31+
32+
33+
#: A `referencing.Registry` containing all official jsonschema resources
34+
#: plus openapi resources.
35+
REGISTRY = (_load_schemas() @ JSONSCHEMA_REGISTRY).crawl()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$id": "https://spec.openapis.org/oas/3.1/dialect/base",
3+
"$schema": "https://json-schema.org/draft/2020-12/schema",
4+
5+
"title": "OpenAPI 3.1 Schema Object Dialect",
6+
"description": "A JSON Schema dialect describing schemas found in OpenAPI documents",
7+
8+
"$vocabulary": {
9+
"https://json-schema.org/draft/2020-12/vocab/core": true,
10+
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
11+
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
12+
"https://json-schema.org/draft/2020-12/vocab/validation": true,
13+
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
14+
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
15+
"https://json-schema.org/draft/2020-12/vocab/content": true,
16+
"https://spec.openapis.org/oas/3.1/vocab/base": false
17+
},
18+
19+
"$dynamicAnchor": "meta",
20+
21+
"allOf": [
22+
{ "$ref": "https://json-schema.org/draft/2020-12/schema" },
23+
{ "$ref": "https://spec.openapis.org/oas/3.1/meta/base" }
24+
]
25+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
{
2+
"$id": "https://spec.openapis.org/oas/3.1/meta/base",
3+
"$schema": "https://json-schema.org/draft/2020-12/schema",
4+
5+
"title": "OAS Base vocabulary",
6+
"description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect",
7+
8+
"$vocabulary": {
9+
"https://spec.openapis.org/oas/3.1/vocab/base": true
10+
},
11+
12+
"$dynamicAnchor": "meta",
13+
14+
"type": ["object", "boolean"],
15+
"properties": {
16+
"example": true,
17+
"discriminator": { "$ref": "#/$defs/discriminator" },
18+
"externalDocs": { "$ref": "#/$defs/external-docs" },
19+
"xml": { "$ref": "#/$defs/xml" }
20+
},
21+
22+
"$defs": {
23+
"extensible": {
24+
"patternProperties": {
25+
"^x-": true
26+
}
27+
},
28+
29+
"discriminator": {
30+
"$ref": "#/$defs/extensible",
31+
"type": "object",
32+
"properties": {
33+
"propertyName": {
34+
"type": "string"
35+
},
36+
"mapping": {
37+
"type": "object",
38+
"additionalProperties": {
39+
"type": "string"
40+
}
41+
}
42+
},
43+
"required": ["propertyName"],
44+
"unevaluatedProperties": false
45+
},
46+
47+
"external-docs": {
48+
"$ref": "#/$defs/extensible",
49+
"type": "object",
50+
"properties": {
51+
"url": {
52+
"type": "string",
53+
"format": "uri-reference"
54+
},
55+
"description": {
56+
"type": "string"
57+
}
58+
},
59+
"required": ["url"],
60+
"unevaluatedProperties": false
61+
},
62+
63+
"xml": {
64+
"$ref": "#/$defs/extensible",
65+
"type": "object",
66+
"properties": {
67+
"name": {
68+
"type": "string"
69+
},
70+
"namespace": {
71+
"type": "string",
72+
"format": "uri"
73+
},
74+
"prefix": {
75+
"type": "string"
76+
},
77+
"attribute": {
78+
"type": "boolean"
79+
},
80+
"wrapped": {
81+
"type": "boolean"
82+
}
83+
},
84+
"unevaluatedProperties": false
85+
}
86+
}
87+
}

openapi_schema_validator/shortcuts.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from jsonschema.exceptions import best_match
66
from jsonschema.protocols import Validator
77

8+
from openapi_schema_validator.validators import OAS31_BASE_DIALECT_ID
89
from openapi_schema_validator.validators import OAS31Validator
10+
from openapi_schema_validator.validators import check_openapi_schema
911

1012

1113
def validate(
@@ -19,7 +21,19 @@ def validate(
1921
Validate an instance against a given schema using the specified validator class.
2022
"""
2123
schema_dict = cast(dict[str, Any], schema)
22-
cls.check_schema(schema_dict)
24+
25+
meta_schema = getattr(cls, "META_SCHEMA", None)
26+
# jsonschema's default check_schema path does not accept a custom
27+
# registry, so for the OAS 3.1 dialect we use the package registry
28+
# explicitly to keep metaschema resolution local and deterministic.
29+
if (
30+
isinstance(meta_schema, dict)
31+
and meta_schema.get("$id") == OAS31_BASE_DIALECT_ID
32+
):
33+
check_openapi_schema(cls, schema_dict)
34+
else:
35+
cls.check_schema(schema_dict)
36+
2337
validator = cls(schema_dict, *args, **kwargs)
2438
error = best_match(
2539
validator.evolve(schema=schema_dict).iter_errors(instance)

0 commit comments

Comments
 (0)