Skip to content

Commit c1510d5

Browse files
committed
Rust backend
1 parent a093f9c commit c1510d5

File tree

13 files changed

+467
-35
lines changed

13 files changed

+467
-35
lines changed

.github/workflows/python-tests.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ on:
1010

1111
jobs:
1212
tests:
13-
name: "py${{ matrix.python-version }}-${{ matrix.os }}"
13+
name: "py${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.backend }}"
1414
runs-on: ${{ matrix.os }}
1515
strategy:
1616
matrix:
1717
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
1818
os: [windows-latest, ubuntu-latest]
19+
backend: ['jsonschema']
20+
include:
21+
- python-version: '3.12'
22+
os: ubuntu-latest
23+
backend: 'jsonschema-rs'
24+
- python-version: '3.13'
25+
os: windows-latest
26+
backend: 'jsonschema-rs'
1927
fail-fast: false
2028
steps:
2129
- uses: actions/checkout@v4
@@ -49,9 +57,14 @@ jobs:
4957
- name: Install dependencies
5058
run: poetry install --all-extras
5159

60+
- name: Install jsonschema-rs
61+
if: matrix.backend != 'jsonschema'
62+
run: poetry run pip install ${{ matrix.backend }}
63+
5264
- name: Test
5365
env:
5466
PYTEST_ADDOPTS: "--color=yes"
67+
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND: ${{ matrix.backend }}
5568
run: poetry run pytest
5669

5770
- name: Static type check

README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ Rules:
131131
* Set ``0`` to disable the resolved cache.
132132
* Invalid values (non-integer or negative) fall back to ``128``.
133133

134+
You can also choose schema validator backend:
135+
136+
.. code-block:: bash
137+
138+
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND=jsonschema-rs
139+
140+
Allowed values are ``auto`` (default), ``jsonschema``, and
141+
``jsonschema-rs``.
142+
Invalid values raise a warning and fall back to ``auto``.
143+
134144
Related projects
135145
################
136146

docs/cli.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ Performance note:
7373
You can tune resolved-path caching with
7474
``OPENAPI_SPEC_VALIDATOR_RESOLVED_CACHE_MAXSIZE``.
7575
Default is ``128``; set ``0`` to disable.
76+
77+
You can also select schema validator backend with
78+
``OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND``
79+
(``auto``/``jsonschema``/``jsonschema-rs``).

docs/python.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,13 @@ Rules:
7575
* Default is ``128``.
7676
* Set ``0`` to disable the resolved cache.
7777
* Invalid values (non-integer or negative) fall back to ``128``.
78+
79+
Schema validator backend can be selected with:
80+
81+
.. code-block:: bash
82+
83+
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND=jsonschema-rs
84+
85+
Allowed values are ``auto`` (default), ``jsonschema``, and
86+
``jsonschema-rs``.
87+
Invalid values raise a warning and fall back to ``auto``.

openapi_spec_validator/__main__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from jsonschema.exceptions import best_match
99

1010
from openapi_spec_validator import __version__
11+
from openapi_spec_validator import schemas
1112
from openapi_spec_validator.readers import read_from_filename
1213
from openapi_spec_validator.readers import read_from_stdin
1314
from openapi_spec_validator.shortcuts import get_validator_cls
@@ -38,6 +39,7 @@ def print_validationerror(
3839
exc: ValidationError,
3940
subschema_errors: str = "best-match",
4041
index: int | None = None,
42+
supports_subschema_details: bool = True,
4143
) -> None:
4244
if index is None:
4345
print(f"{filename}: Validation Error: {exc}")
@@ -48,6 +50,13 @@ def print_validationerror(
4850
print(exc.cause)
4951
if not exc.context:
5052
return
53+
if not supports_subschema_details:
54+
print("\n\n# Subschema details\n")
55+
print(
56+
"Subschema error details are not available "
57+
"with jsonschema-rs backend."
58+
)
59+
return
5160
if subschema_errors == "all":
5261
print("\n\n# Due to one of those errors\n")
5362
print("\n\n\n".join("## " + str(e) for e in exc.context))
@@ -139,6 +148,10 @@ def main(args: Sequence[str] | None = None) -> None:
139148
if subschema_errors is None:
140149
subschema_errors = "best-match"
141150

151+
supports_subschema_details = (
152+
schemas.get_validator_backend() != "jsonschema-rs"
153+
)
154+
142155
for filename in args_parsed.file:
143156
# choose source
144157
reader = read_from_filename
@@ -181,6 +194,9 @@ def main(args: Sequence[str] | None = None) -> None:
181194
err,
182195
subschema_errors,
183196
index=idx,
197+
supports_subschema_details=(
198+
supports_subschema_details
199+
),
184200
)
185201
print(f"{filename}: {len(errors)} validation errors found")
186202
sys.exit(1)
@@ -189,7 +205,12 @@ def main(args: Sequence[str] | None = None) -> None:
189205

190206
validate(spec, base_uri=base_uri, cls=validator_cls)
191207
except ValidationError as exc:
192-
print_validationerror(filename, exc, subschema_errors)
208+
print_validationerror(
209+
filename,
210+
exc,
211+
subschema_errors,
212+
supports_subschema_details=supports_subschema_details,
213+
)
193214
sys.exit(1)
194215
except Exception as exc:
195216
print_error(filename, exc)
Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
"""OpenAIP spec validator schemas module."""
1+
"""OpenAPI spec validator schemas module."""
22

33
from functools import partial
44

5-
from jsonschema.validators import Draft4Validator
6-
from jsonschema.validators import Draft202012Validator
75
from lazy_object_proxy import Proxy
86

7+
from openapi_spec_validator.schemas.backend import get_validator_for
8+
from openapi_spec_validator.schemas.backend import get_validator_backend
99
from openapi_spec_validator.schemas.utils import get_schema_content
1010

11-
__all__ = ["schema_v2", "schema_v3", "schema_v30", "schema_v31", "schema_v32"]
11+
__all__ = [
12+
"schema_v2",
13+
"schema_v3",
14+
"schema_v30",
15+
"schema_v31",
16+
"schema_v32",
17+
"get_validator_backend",
18+
]
1219

1320
get_schema_content_v2 = partial(get_schema_content, "2.0")
1421
get_schema_content_v30 = partial(get_schema_content, "3.0")
@@ -23,12 +30,7 @@
2330
# alias to the latest v3 version
2431
schema_v3 = schema_v32
2532

26-
get_openapi_v2_schema_validator = partial(Draft4Validator, schema_v2)
27-
get_openapi_v30_schema_validator = partial(Draft4Validator, schema_v30)
28-
get_openapi_v31_schema_validator = partial(Draft202012Validator, schema_v31)
29-
get_openapi_v32_schema_validator = partial(Draft202012Validator, schema_v32)
30-
31-
openapi_v2_schema_validator = Proxy(get_openapi_v2_schema_validator)
32-
openapi_v30_schema_validator = Proxy(get_openapi_v30_schema_validator)
33-
openapi_v31_schema_validator = Proxy(get_openapi_v31_schema_validator)
34-
openapi_v32_schema_validator = Proxy(get_openapi_v32_schema_validator)
33+
openapi_v2_schema_validator = Proxy(partial(get_validator_for, schema_v2))
34+
openapi_v30_schema_validator = Proxy(partial(get_validator_for, schema_v30))
35+
openapi_v31_schema_validator = Proxy(partial(get_validator_for, schema_v31))
36+
openapi_v32_schema_validator = Proxy(partial(get_validator_for, schema_v32))
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Schema validator backend selection and factories."""
2+
3+
from typing import Any
4+
5+
from jsonschema.validators import validator_for
6+
7+
from openapi_spec_validator.schemas.backend.jsonschema_rs import (
8+
create_validator as create_jsonschema_rs_validator,
9+
)
10+
from openapi_spec_validator.schemas.backend.jsonschema_rs import (
11+
has_jsonschema_rs_validators,
12+
)
13+
from openapi_spec_validator.settings import get_schema_validator_backend
14+
15+
16+
def _use_jsonschema_rs() -> bool:
17+
backend_mode = get_schema_validator_backend()
18+
available = has_jsonschema_rs_validators()
19+
20+
if backend_mode == "jsonschema":
21+
return False
22+
if backend_mode == "jsonschema-rs":
23+
if not available:
24+
raise RuntimeError(
25+
"OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND="
26+
"jsonschema-rs is set but jsonschema-rs is not available. "
27+
"Install it with: pip install jsonschema-rs"
28+
)
29+
return True
30+
return available
31+
32+
33+
def get_validator_backend() -> str:
34+
if _use_jsonschema_rs():
35+
return "jsonschema-rs"
36+
return "jsonschema"
37+
38+
39+
def create_jsonschema_validator(schema: dict[str, Any]) -> Any:
40+
validator_cls = validator_for(schema)
41+
return validator_cls(schema)
42+
43+
44+
def get_validator_for(schema) -> Any:
45+
if _use_jsonschema_rs():
46+
return create_jsonschema_rs_validator(dict(schema))
47+
return create_jsonschema_validator(schema)
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""
2+
jsonschema-rs adapter for openapi-spec-validator.
3+
4+
This module provides a compatibility layer between jsonschema-rs (Rust)
5+
and the existing jsonschema (Python) validator interface.
6+
"""
7+
8+
import importlib
9+
from typing import TYPE_CHECKING
10+
from typing import Any
11+
from typing import Iterator
12+
from typing import cast
13+
14+
if TYPE_CHECKING:
15+
16+
class ValidationErrorBase(Exception):
17+
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
18+
19+
else:
20+
from jsonschema.exceptions import ValidationError as ValidationErrorBase
21+
22+
# Try to import jsonschema-rs
23+
jsonschema_rs: Any = None
24+
try:
25+
jsonschema_rs = importlib.import_module("jsonschema_rs")
26+
27+
HAS_JSONSCHEMA_RS = True
28+
except ImportError:
29+
HAS_JSONSCHEMA_RS = False
30+
31+
32+
def _get_jsonschema_rs_module() -> Any:
33+
if jsonschema_rs is None:
34+
raise ImportError(
35+
"jsonschema-rs is not installed. Install it with: "
36+
"pip install jsonschema-rs"
37+
)
38+
return jsonschema_rs
39+
40+
41+
class RustValidatorError(ValidationErrorBase):
42+
"""ValidationError compatible with jsonschema, but originating from Rust validator."""
43+
44+
pass
45+
46+
47+
class RustValidatorWrapper:
48+
"""
49+
Wrapper that makes jsonschema-rs validator compatible with jsonschema interface.
50+
51+
This allows drop-in replacement while maintaining the same API surface.
52+
"""
53+
54+
def __init__(self, schema: dict[str, Any], validator: Any):
55+
"""
56+
Initialize Rust validator wrapper.
57+
58+
Args:
59+
schema: JSON Schema to validate against
60+
cls: JSON Schema validator
61+
"""
62+
if not HAS_JSONSCHEMA_RS:
63+
raise ImportError(
64+
"jsonschema-rs is not installed. Install it with: "
65+
"pip install jsonschema-rs"
66+
)
67+
68+
self.schema = schema
69+
self._rs_validator = validator
70+
71+
def iter_errors(self, instance: Any) -> Iterator[ValidationErrorBase]:
72+
"""
73+
Validate instance and yield errors in jsonschema format.
74+
75+
This method converts jsonschema-rs errors to jsonschema ValidationError
76+
format for compatibility with existing code.
77+
"""
78+
for error in self._rs_validator.iter_errors(instance):
79+
yield self._convert_rust_error(error, instance)
80+
81+
def validate(self, instance: Any) -> None:
82+
"""
83+
Validate instance and raise ValidationError if invalid.
84+
85+
Compatible with jsonschema Validator.validate() method.
86+
"""
87+
try:
88+
self._rs_validator.validate(instance)
89+
except _get_jsonschema_rs_module().ValidationError as e:
90+
# Convert and raise as Python ValidationError
91+
py_error = self._convert_rust_error_exception(e, instance)
92+
raise py_error from e
93+
94+
def is_valid(self, instance: Any) -> bool:
95+
"""Check if instance is valid against schema."""
96+
return cast(bool, self._rs_validator.is_valid(instance))
97+
98+
def _convert_rust_error(
99+
self, rust_error: Any, instance: Any
100+
) -> ValidationErrorBase:
101+
"""
102+
Convert jsonschema-rs error format to jsonschema ValidationError.
103+
104+
jsonschema-rs error structure:
105+
- message: str
106+
- instance_path: list
107+
- schema_path: list (if available)
108+
"""
109+
message = str(rust_error)
110+
111+
# Extract path information if available
112+
# Note: jsonschema-rs error format may differ - adjust as needed
113+
instance_path = getattr(rust_error, "instance_path", [])
114+
schema_path = getattr(rust_error, "schema_path", [])
115+
116+
return RustValidatorError(
117+
message=message,
118+
path=list(instance_path) if instance_path else [],
119+
schema_path=list(schema_path) if schema_path else [],
120+
instance=instance,
121+
schema=self.schema,
122+
)
123+
124+
def _convert_rust_error_exception(
125+
self, rust_error: Any, instance: Any
126+
) -> ValidationErrorBase:
127+
"""Convert jsonschema-rs ValidationError exception to Python format."""
128+
message = str(rust_error)
129+
130+
return RustValidatorError(
131+
message=message,
132+
instance=instance,
133+
schema=self.schema,
134+
)
135+
136+
137+
def create_validator(schema: dict[str, Any]) -> RustValidatorWrapper:
138+
"""
139+
Factory function to create Rust-backed validator.
140+
141+
Args:
142+
schema: JSON Schema to validate against
143+
144+
Returns:
145+
RustValidatorWrapper instance
146+
"""
147+
148+
# Create appropriate Rust validator based on draft
149+
module = _get_jsonschema_rs_module()
150+
validator_cls: Any = module.validator_cls_for(schema)
151+
152+
validator = validator_cls(
153+
schema,
154+
validate_formats=True,
155+
)
156+
return RustValidatorWrapper(schema, validator=validator)
157+
158+
159+
# Convenience function to check if Rust validators are available
160+
def has_jsonschema_rs_validators() -> bool:
161+
"""Check if jsonschema-rs is available."""
162+
return HAS_JSONSCHEMA_RS

0 commit comments

Comments
 (0)