Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@ To validate an OpenAPI v3.1 schema:

By default, the latest OpenAPI schema syntax is expected.

The OpenAPI 3.1 base dialect URI is registered for
``jsonschema.validators.validator_for`` resolution.
Schemas declaring
``"$schema": "https://spec.openapis.org/oas/3.1/dialect/base"``
resolve directly to ``OAS31Validator`` without unresolved-metaschema
fallback warnings.

.. code-block:: python

from jsonschema.validators import validator_for

from openapi_schema_validator import OAS31Validator

schema = {
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"type": "object",
}

assert validator_for(schema) is OAS31Validator


Strict vs Pragmatic Validators
==============================
Expand Down
22 changes: 21 additions & 1 deletion docs/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ if you want to disambiguate the expected schema version, import and use ``OAS31V

validate({"name": "John", "age": 23}, schema, cls=OAS31Validator)

The OpenAPI 3.1 base dialect URI is registered for
``jsonschema.validators.validator_for`` resolution.
If your schema declares
``"$schema": "https://spec.openapis.org/oas/3.1/dialect/base"``,
``validator_for`` resolves directly to ``OAS31Validator`` without
unresolved-metaschema fallback warnings.

.. code-block:: python

from jsonschema.validators import validator_for

from openapi_schema_validator import OAS31Validator

schema = {
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"type": "object",
}
assert validator_for(schema) is OAS31Validator

For OpenAPI 3.2, use ``OAS32Validator`` (behaves identically to ``OAS31Validator``, since 3.2 uses the same JSON Schema dialect).

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

.. code-block:: python

from openapi_schema_validator import OAS30Validator, OAS30StrictValidator
from openapi_schema_validator import OAS30StrictValidator
from openapi_schema_validator import OAS30Validator

# Pragmatic (default) - accepts bytes for binary format
validator = OAS30Validator({"type": "string", "format": "binary"})
Expand Down
41 changes: 41 additions & 0 deletions openapi_schema_validator/_dialects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Any

from jsonschema.validators import validates

from openapi_schema_validator._specifications import (
REGISTRY as OPENAPI_SPECIFICATIONS,
)

__all__ = [
"OAS31_BASE_DIALECT_ID",
"OAS31_BASE_DIALECT_METASCHEMA",
"register_openapi_dialect",
]

OAS31_BASE_DIALECT_ID = "https://spec.openapis.org/oas/3.1/dialect/base"
OAS31_BASE_DIALECT_METASCHEMA = OPENAPI_SPECIFICATIONS.contents(
OAS31_BASE_DIALECT_ID,
)

_REGISTERED_VALIDATORS: dict[tuple[str, str], Any] = {}


def register_openapi_dialect(
*,
validator: Any,
dialect_id: str,
version_name: str,
metaschema: Any,
) -> Any:
key = (dialect_id, version_name)
registered_validator = _REGISTERED_VALIDATORS.get(key)

if registered_validator is validator:
return validator
if registered_validator is not None:
return registered_validator

validator.META_SCHEMA = metaschema
validator = validates(version_name)(validator)
_REGISTERED_VALIDATORS[key] = validator
return validator
35 changes: 35 additions & 0 deletions openapi_schema_validator/_specifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json
from importlib.resources import files
from typing import Any
from typing import Iterator

from jsonschema_specifications import REGISTRY as JSONSCHEMA_REGISTRY
from referencing import Resource

__all__ = ["REGISTRY"]


def _iter_schema_files() -> Iterator[Any]:
schema_root = files(__package__).joinpath("schemas")
stack = [schema_root]

while stack:
current = stack.pop()
for child in current.iterdir():
if child.name.startswith("."):
continue
if child.is_dir():
stack.append(child)
continue
yield child


def _load_schemas() -> Iterator[Resource]:
for path in _iter_schema_files():
contents = json.loads(path.read_text(encoding="utf-8"))
yield Resource.from_contents(contents)


#: A `referencing.Registry` containing all official jsonschema resources
#: plus openapi resources.
REGISTRY = (_load_schemas() @ JSONSCHEMA_REGISTRY).crawl()
25 changes: 25 additions & 0 deletions openapi_schema_validator/schemas/oas3.1/metaschema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$id": "https://spec.openapis.org/oas/3.1/dialect/base",
"$schema": "https://json-schema.org/draft/2020-12/schema",

"title": "OpenAPI 3.1 Schema Object Dialect",
"description": "A JSON Schema dialect describing schemas found in OpenAPI documents",

"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
"https://json-schema.org/draft/2020-12/vocab/content": true,
"https://spec.openapis.org/oas/3.1/vocab/base": false
},

"$dynamicAnchor": "meta",

"allOf": [
{ "$ref": "https://json-schema.org/draft/2020-12/schema" },
{ "$ref": "https://spec.openapis.org/oas/3.1/meta/base" }
]
}
87 changes: 87 additions & 0 deletions openapi_schema_validator/schemas/oas3.1/vocabularies/base
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"$id": "https://spec.openapis.org/oas/3.1/meta/base",
"$schema": "https://json-schema.org/draft/2020-12/schema",

"title": "OAS Base vocabulary",
"description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect",

"$vocabulary": {
"https://spec.openapis.org/oas/3.1/vocab/base": true
},

"$dynamicAnchor": "meta",

"type": ["object", "boolean"],
"properties": {
"example": true,
"discriminator": { "$ref": "#/$defs/discriminator" },
"externalDocs": { "$ref": "#/$defs/external-docs" },
"xml": { "$ref": "#/$defs/xml" }
},

"$defs": {
"extensible": {
"patternProperties": {
"^x-": true
}
},

"discriminator": {
"$ref": "#/$defs/extensible",
"type": "object",
"properties": {
"propertyName": {
"type": "string"
},
"mapping": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["propertyName"],
"unevaluatedProperties": false
},

"external-docs": {
"$ref": "#/$defs/extensible",
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri-reference"
},
"description": {
"type": "string"
}
},
"required": ["url"],
"unevaluatedProperties": false
},

"xml": {
"$ref": "#/$defs/extensible",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"namespace": {
"type": "string",
"format": "uri"
},
"prefix": {
"type": "string"
},
"attribute": {
"type": "boolean"
},
"wrapped": {
"type": "boolean"
}
},
"unevaluatedProperties": false
}
}
}
16 changes: 15 additions & 1 deletion openapi_schema_validator/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from jsonschema.exceptions import best_match
from jsonschema.protocols import Validator

from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_ID
from openapi_schema_validator.validators import OAS31Validator
from openapi_schema_validator.validators import check_openapi_schema


def validate(
Expand All @@ -19,7 +21,19 @@ def validate(
Validate an instance against a given schema using the specified validator class.
"""
schema_dict = cast(dict[str, Any], schema)
cls.check_schema(schema_dict)

meta_schema = getattr(cls, "META_SCHEMA", None)
# jsonschema's default check_schema path does not accept a custom
# registry, so for the OAS 3.1 dialect we use the package registry
# explicitly to keep metaschema resolution local and deterministic.
if (
isinstance(meta_schema, dict)
and meta_schema.get("$id") == OAS31_BASE_DIALECT_ID
):
check_openapi_schema(cls, schema_dict)
else:
cls.check_schema(schema_dict)

validator = cls(schema_dict, *args, **kwargs)
error = best_match(
validator.evolve(schema=schema_dict).iter_errors(instance)
Expand Down
Loading
Loading