Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ on:

jobs:
tests:
name: "py${{ matrix.python-version }}-${{ matrix.os }}"
name: "py${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.backend }}"
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
os: [windows-latest, ubuntu-latest]
backend: ['jsonschema']
include:
- python-version: '3.12'
os: ubuntu-latest
backend: 'jsonschema-rs'
- python-version: '3.13'
os: windows-latest
backend: 'jsonschema-rs'
fail-fast: false
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -49,9 +57,14 @@ jobs:
- name: Install dependencies
run: poetry install --all-extras

- name: Install jsonschema-rs
if: matrix.backend != 'jsonschema'
run: poetry run pip install ${{ matrix.backend }}

- name: Test
env:
PYTEST_ADDOPTS: "--color=yes"
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND: ${{ matrix.backend }}
run: poetry run pytest

- name: Static type check
Expand Down
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ Rules:
* Set ``0`` to disable the resolved cache.
* Invalid values (non-integer or negative) fall back to ``128``.

You can also choose schema validator backend:

.. code-block:: bash
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND=jsonschema-rs
Allowed values are ``auto`` (default), ``jsonschema``, and
``jsonschema-rs``.
Invalid values raise a warning and fall back to ``auto``.

Related projects
################

Expand Down
4 changes: 4 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@ Performance note:
You can tune resolved-path caching with
``OPENAPI_SPEC_VALIDATOR_RESOLVED_CACHE_MAXSIZE``.
Default is ``128``; set ``0`` to disable.

You can also select schema validator backend with
``OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND``
(``auto``/``jsonschema``/``jsonschema-rs``).
10 changes: 10 additions & 0 deletions docs/python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,13 @@ Rules:
* Default is ``128``.
* Set ``0`` to disable the resolved cache.
* Invalid values (non-integer or negative) fall back to ``128``.

Schema validator backend can be selected with:

.. code-block:: bash
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND=jsonschema-rs
Allowed values are ``auto`` (default), ``jsonschema``, and
``jsonschema-rs``.
Invalid values raise a warning and fall back to ``auto``.
23 changes: 22 additions & 1 deletion openapi_spec_validator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from jsonschema.exceptions import best_match

from openapi_spec_validator import __version__
from openapi_spec_validator import schemas
from openapi_spec_validator.readers import read_from_filename
from openapi_spec_validator.readers import read_from_stdin
from openapi_spec_validator.shortcuts import get_validator_cls
Expand Down Expand Up @@ -38,6 +39,7 @@ def print_validationerror(
exc: ValidationError,
subschema_errors: str = "best-match",
index: int | None = None,
supports_subschema_details: bool = True,
) -> None:
if index is None:
print(f"{filename}: Validation Error: {exc}")
Expand All @@ -48,6 +50,13 @@ def print_validationerror(
print(exc.cause)
if not exc.context:
return
if not supports_subschema_details:
print("\n\n# Subschema details\n")
print(
"Subschema error details are not available "
"with jsonschema-rs backend."
)
return
if subschema_errors == "all":
print("\n\n# Due to one of those errors\n")
print("\n\n\n".join("## " + str(e) for e in exc.context))
Expand Down Expand Up @@ -139,6 +148,10 @@ def main(args: Sequence[str] | None = None) -> None:
if subschema_errors is None:
subschema_errors = "best-match"

supports_subschema_details = (
schemas.get_validator_backend() != "jsonschema-rs"
)

for filename in args_parsed.file:
# choose source
reader = read_from_filename
Expand Down Expand Up @@ -181,6 +194,9 @@ def main(args: Sequence[str] | None = None) -> None:
err,
subschema_errors,
index=idx,
supports_subschema_details=(
supports_subschema_details
),
)
print(f"{filename}: {len(errors)} validation errors found")
sys.exit(1)
Expand All @@ -189,7 +205,12 @@ def main(args: Sequence[str] | None = None) -> None:

validate(spec, base_uri=base_uri, cls=validator_cls)
except ValidationError as exc:
print_validationerror(filename, exc, subschema_errors)
print_validationerror(
filename,
exc,
subschema_errors,
supports_subschema_details=supports_subschema_details,
)
sys.exit(1)
except Exception as exc:
print_error(filename, exc)
Expand Down
28 changes: 15 additions & 13 deletions openapi_spec_validator/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
"""OpenAIP spec validator schemas module."""
"""OpenAPI spec validator schemas module."""

from functools import partial

from jsonschema.validators import Draft4Validator
from jsonschema.validators import Draft202012Validator
from lazy_object_proxy import Proxy

from openapi_spec_validator.schemas.backend import get_validator_backend
from openapi_spec_validator.schemas.backend import get_validator_for
from openapi_spec_validator.schemas.utils import get_schema_content

__all__ = ["schema_v2", "schema_v3", "schema_v30", "schema_v31", "schema_v32"]
__all__ = [
"schema_v2",
"schema_v3",
"schema_v30",
"schema_v31",
"schema_v32",
"get_validator_backend",
]

get_schema_content_v2 = partial(get_schema_content, "2.0")
get_schema_content_v30 = partial(get_schema_content, "3.0")
Expand All @@ -23,12 +30,7 @@
# alias to the latest v3 version
schema_v3 = schema_v32

get_openapi_v2_schema_validator = partial(Draft4Validator, schema_v2)
get_openapi_v30_schema_validator = partial(Draft4Validator, schema_v30)
get_openapi_v31_schema_validator = partial(Draft202012Validator, schema_v31)
get_openapi_v32_schema_validator = partial(Draft202012Validator, schema_v32)

openapi_v2_schema_validator = Proxy(get_openapi_v2_schema_validator)
openapi_v30_schema_validator = Proxy(get_openapi_v30_schema_validator)
openapi_v31_schema_validator = Proxy(get_openapi_v31_schema_validator)
openapi_v32_schema_validator = Proxy(get_openapi_v32_schema_validator)
openapi_v2_schema_validator = Proxy(partial(get_validator_for, schema_v2))
openapi_v30_schema_validator = Proxy(partial(get_validator_for, schema_v30))
openapi_v31_schema_validator = Proxy(partial(get_validator_for, schema_v31))
openapi_v32_schema_validator = Proxy(partial(get_validator_for, schema_v32))
43 changes: 43 additions & 0 deletions openapi_spec_validator/schemas/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Schema validator backend selection and factories."""

from typing import Any

from openapi_spec_validator.schemas.backend.jsonschema import (
create_validator as create_jsonschema_validator,
)
from openapi_spec_validator.schemas.backend.jsonschema_rs import (
create_validator as create_jsonschema_rs_validator,
)
from openapi_spec_validator.schemas.backend.jsonschema_rs import (
has_jsonschema_rs_validators,
)
from openapi_spec_validator.settings import get_schema_validator_backend


def _use_jsonschema_rs() -> bool:
backend_mode = get_schema_validator_backend()
available = has_jsonschema_rs_validators()

if backend_mode == "jsonschema":
return False
if backend_mode == "jsonschema-rs":
if not available:
raise RuntimeError(
"OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND="
"jsonschema-rs is set but jsonschema-rs is not available. "
"Install it with: pip install jsonschema-rs"
)
return True
return available


def get_validator_backend() -> str:
if _use_jsonschema_rs():
return "jsonschema-rs"
return "jsonschema"


def get_validator_for(schema: dict[str, Any]) -> Any:
if _use_jsonschema_rs():
return create_jsonschema_rs_validator(dict(schema))
return create_jsonschema_validator(schema)
8 changes: 8 additions & 0 deletions openapi_spec_validator/schemas/backend/jsonschema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from typing import Any

from jsonschema.validators import validator_for


def create_validator(schema: dict[str, Any]) -> Any:
validator_cls = validator_for(schema)
return validator_cls(schema)
162 changes: 162 additions & 0 deletions openapi_spec_validator/schemas/backend/jsonschema_rs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""
jsonschema-rs adapter for openapi-spec-validator.

This module provides a compatibility layer between jsonschema-rs (Rust)
and the existing jsonschema (Python) validator interface.
"""

import importlib
from collections.abc import Iterator
from typing import TYPE_CHECKING
from typing import Any
from typing import cast

if TYPE_CHECKING:

class ValidationErrorBase(Exception):
def __init__(self, *args: Any, **kwargs: Any) -> None: ...

else:
from jsonschema.exceptions import ValidationError as ValidationErrorBase

# Try to import jsonschema-rs
jsonschema_rs: Any = None
try:
jsonschema_rs = importlib.import_module("jsonschema_rs")

HAS_JSONSCHEMA_RS = True
except ImportError:
HAS_JSONSCHEMA_RS = False


def _get_jsonschema_rs_module() -> Any:
if jsonschema_rs is None:
raise ImportError(
"jsonschema-rs is not installed. Install it with: "
"pip install jsonschema-rs"
)
return jsonschema_rs


class JsonschemaRsValidatorError(ValidationErrorBase):
"""ValidationError compatible with jsonschema, but originating from Rust validator."""

pass


class JsonschemaRsValidatorWrapper:
"""
Wrapper that makes jsonschema-rs validator compatible with jsonschema interface.

This allows drop-in replacement while maintaining the same API surface.
"""

def __init__(self, schema: dict[str, Any], validator: Any):
"""
Initialize Rust validator wrapper.

Args:
schema: JSON Schema to validate against
cls: JSON Schema validator
"""
if not HAS_JSONSCHEMA_RS:
raise ImportError(
"jsonschema-rs is not installed. Install it with: "
"pip install jsonschema-rs"
)

self.schema = schema
self._rs_validator = validator

def iter_errors(self, instance: Any) -> Iterator[ValidationErrorBase]:
"""
Validate instance and yield errors in jsonschema format.

This method converts jsonschema-rs errors to jsonschema ValidationError
format for compatibility with existing code.
"""
for error in self._rs_validator.iter_errors(instance):
yield self._convert_rust_error(error, instance)

def validate(self, instance: Any) -> None:
"""
Validate instance and raise ValidationError if invalid.

Compatible with jsonschema Validator.validate() method.
"""
try:
self._rs_validator.validate(instance)
except _get_jsonschema_rs_module().ValidationError as e:
# Convert and raise as Python ValidationError
py_error = self._convert_rust_error_exception(e, instance)
raise py_error from e

def is_valid(self, instance: Any) -> bool:
"""Check if instance is valid against schema."""
return cast(bool, self._rs_validator.is_valid(instance))

def _convert_rust_error(
self, rust_error: Any, instance: Any
) -> ValidationErrorBase:
"""
Convert jsonschema-rs error format to jsonschema ValidationError.

jsonschema-rs error structure:
- message: str
- instance_path: list
- schema_path: list (if available)
"""
message = str(rust_error)

# Extract path information if available
# Note: jsonschema-rs error format may differ - adjust as needed
instance_path = getattr(rust_error, "instance_path", [])
schema_path = getattr(rust_error, "schema_path", [])

return JsonschemaRsValidatorError(
message=message,
path=list(instance_path) if instance_path else [],
schema_path=list(schema_path) if schema_path else [],
instance=instance,
schema=self.schema,
)

def _convert_rust_error_exception(
self, rust_error: Any, instance: Any
) -> ValidationErrorBase:
"""Convert jsonschema-rs ValidationError exception to Python format."""
message = str(rust_error)

return JsonschemaRsValidatorError(
message=message,
instance=instance,
schema=self.schema,
)


def create_validator(schema: dict[str, Any]) -> JsonschemaRsValidatorWrapper:
"""
Factory function to create Rust-backed validator.

Args:
schema: JSON Schema to validate against

Returns:
JsonschemaRsValidatorWrapper instance
"""

# Create appropriate Rust validator based on draft
module = _get_jsonschema_rs_module()
validator_cls: Any = module.validator_cls_for(schema)

validator = validator_cls(
schema,
validate_formats=True,
)
return JsonschemaRsValidatorWrapper(schema, validator=validator)


# Convenience function to check if Rust validators are available
def has_jsonschema_rs_validators() -> bool:
"""Check if jsonschema-rs is available."""
return HAS_JSONSCHEMA_RS
Loading