From dd6b514fd8bbb5f5ba5bd8e3777c5dfe8ab4900e Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 25 Feb 2026 21:24:58 +0000 Subject: [PATCH 1/2] Support official OAS 3.2 dialect and set it as default --- README.rst | 19 ++- docs/format.rst | 2 +- docs/index.rst | 2 +- docs/validation.rst | 17 ++- openapi_schema_validator/__init__.py | 4 + openapi_schema_validator/_dialects.py | 6 + .../schemas/oas3.2/dialect/2025-09-17.json | 25 ++++ .../schemas/oas3.2/meta/2025-09-17.json | 114 ++++++++++++++++++ openapi_schema_validator/shortcuts.py | 13 +- openapi_schema_validator/validators.py | 27 +++-- tests/integration/test_validators.py | 71 ++++++++++- tests/unit/test_shortcut.py | 15 +++ 12 files changed, 284 insertions(+), 31 deletions(-) create mode 100644 openapi_schema_validator/schemas/oas3.2/dialect/2025-09-17.json create mode 100644 openapi_schema_validator/schemas/oas3.2/meta/2025-09-17.json diff --git a/README.rst b/README.rst index 58e09b7..03f826e 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Openapi-schema-validator is a Python library that validates schema against: * `OpenAPI Schema Specification v3.0 `__ which is an extended subset of the `JSON Schema Specification Wright Draft 00 `__. * `OpenAPI Schema Specification v3.1 `__ which is an extended superset of the `JSON Schema Specification Draft 2020-12 `__. -* `OpenAPI Schema Specification v3.2 `__ which uses the same JSON Schema dialect as v3.1. +* `OpenAPI Schema Specification v3.2 `__ using the published OAS 3.2 JSON Schema dialect resources. Documentation @@ -100,25 +100,32 @@ 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 +The OpenAPI 3.1 and 3.2 base dialect URIs are 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. +Schemas declaring ``"$schema"`` as either +``"https://spec.openapis.org/oas/3.1/dialect/base"`` or +``"https://spec.openapis.org/oas/3.2/dialect/2025-09-17"`` resolve +directly to ``OAS31Validator`` and ``OAS32Validator`` without +unresolved-metaschema fallback warnings. .. code-block:: python from jsonschema.validators import validator_for from openapi_schema_validator import OAS31Validator + from openapi_schema_validator import OAS32Validator schema = { "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", "type": "object", } + schema32 = { + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "type": "object", + } assert validator_for(schema) is OAS31Validator + assert validator_for(schema32) is OAS32Validator Strict vs Pragmatic Validators diff --git a/docs/format.rst b/docs/format.rst index 44899a9..2279856 100644 --- a/docs/format.rst +++ b/docs/format.rst @@ -13,4 +13,4 @@ You can check format for predefined OAS primitive types ... ValidationError: '-12' is not a 'date' -For OpenAPI 3.2, use ``oas32_format_checker`` (behaves identically to ``oas31_format_checker``, since 3.2 uses the same JSON Schema dialect). +For OpenAPI 3.2, use ``oas32_format_checker``. diff --git a/docs/index.rst b/docs/index.rst index 35bb311..8ab4455 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ Openapi-schema-validator is a Python library that validates schema against: * `OpenAPI Schema Specification v3.0 `__ which is an extended subset of the `JSON Schema Specification Wright Draft 00 `__. * `OpenAPI Schema Specification v3.1 `__ which is an extended superset of the `JSON Schema Specification Draft 2020-12 `__. -* `OpenAPI Schema Specification v3.2 `__ which uses the same JSON Schema dialect as v3.1. +* `OpenAPI Schema Specification v3.2 `__ using the published OAS 3.2 JSON Schema dialect resources. Installation ------------ diff --git a/docs/validation.rst b/docs/validation.rst index 3c81323..c93aa69 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -67,26 +67,33 @@ 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 +The OpenAPI 3.1 and 3.2 base dialect URIs are 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. +``"$schema": "https://spec.openapis.org/oas/3.1/dialect/base"`` or +``"$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17"``, +``validator_for`` resolves directly to ``OAS31Validator`` or +``OAS32Validator`` without unresolved-metaschema fallback warnings. .. code-block:: python from jsonschema.validators import validator_for from openapi_schema_validator import OAS31Validator + from openapi_schema_validator import OAS32Validator schema = { "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", "type": "object", } + schema32 = { + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "type": "object", + } assert validator_for(schema) is OAS31Validator + assert validator_for(schema32) is OAS32Validator -For OpenAPI 3.2, use ``OAS32Validator`` (behaves identically to ``OAS31Validator``, since 3.2 uses the same JSON Schema dialect). +For OpenAPI 3.2, use ``OAS32Validator``. In order to validate OpenAPI 3.0 schema, import and use ``OAS30Validator`` instead of ``OAS31Validator``. diff --git a/openapi_schema_validator/__init__.py b/openapi_schema_validator/__init__.py index 11df1cc..50d9441 100644 --- a/openapi_schema_validator/__init__.py +++ b/openapi_schema_validator/__init__.py @@ -1,3 +1,5 @@ +from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_ID +from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID from openapi_schema_validator._format import oas30_format_checker from openapi_schema_validator._format import oas30_strict_format_checker from openapi_schema_validator._format import oas31_format_checker @@ -28,4 +30,6 @@ "oas31_format_checker", "OAS32Validator", "oas32_format_checker", + "OAS31_BASE_DIALECT_ID", + "OAS32_BASE_DIALECT_ID", ] diff --git a/openapi_schema_validator/_dialects.py b/openapi_schema_validator/_dialects.py index 836724c..f301ed0 100644 --- a/openapi_schema_validator/_dialects.py +++ b/openapi_schema_validator/_dialects.py @@ -9,6 +9,8 @@ __all__ = [ "OAS31_BASE_DIALECT_ID", "OAS31_BASE_DIALECT_METASCHEMA", + "OAS32_BASE_DIALECT_ID", + "OAS32_BASE_DIALECT_METASCHEMA", "register_openapi_dialect", ] @@ -16,6 +18,10 @@ OAS31_BASE_DIALECT_METASCHEMA = OPENAPI_SPECIFICATIONS.contents( OAS31_BASE_DIALECT_ID, ) +OAS32_BASE_DIALECT_ID = "https://spec.openapis.org/oas/3.2/dialect/2025-09-17" +OAS32_BASE_DIALECT_METASCHEMA = OPENAPI_SPECIFICATIONS.contents( + OAS32_BASE_DIALECT_ID, +) _REGISTERED_VALIDATORS: dict[tuple[str, str], Any] = {} diff --git a/openapi_schema_validator/schemas/oas3.2/dialect/2025-09-17.json b/openapi_schema_validator/schemas/oas3.2/dialect/2025-09-17.json new file mode 100644 index 0000000..46589c7 --- /dev/null +++ b/openapi_schema_validator/schemas/oas3.2/dialect/2025-09-17.json @@ -0,0 +1,25 @@ +{ + "$id": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OpenAPI 3.2 Schema Object Dialect", + "description": "A JSON Schema dialect describing schemas found in OpenAPI v3.2.x Descriptions", + "$dynamicAnchor": "meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://spec.openapis.org/oas/3.2/vocab/base": false + }, + "allOf": [ + { + "$ref": "https://json-schema.org/draft/2020-12/schema" + }, + { + "$ref": "https://spec.openapis.org/oas/3.2/meta/2025-09-17" + } + ] +} diff --git a/openapi_schema_validator/schemas/oas3.2/meta/2025-09-17.json b/openapi_schema_validator/schemas/oas3.2/meta/2025-09-17.json new file mode 100644 index 0000000..ca60da3 --- /dev/null +++ b/openapi_schema_validator/schemas/oas3.2/meta/2025-09-17.json @@ -0,0 +1,114 @@ +{ + "$id": "https://spec.openapis.org/oas/3.2/meta/2025-09-17", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OAS Base Vocabulary", + "description": "A JSON Schema Vocabulary used in the OpenAPI JSON Schema Dialect", + "$dynamicAnchor": "meta", + "$vocabulary": { + "https://spec.openapis.org/oas/3.2/vocab/base": true + }, + "type": [ + "object", + "boolean" + ], + "properties": { + "discriminator": { + "$ref": "#/$defs/discriminator" + }, + "example": { + "deprecated": true + }, + "externalDocs": { + "$ref": "#/$defs/external-docs" + }, + "xml": { + "$ref": "#/$defs/xml" + } + }, + "$defs": { + "discriminator": { + "$ref": "#/$defs/extensible", + "properties": { + "mapping": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "defaultMapping": { + "type": "string" + }, + "propertyName": { + "type": "string" + } + }, + "type": "object", + "unevaluatedProperties": false + }, + "extensible": { + "patternProperties": { + "^x-": true + } + }, + "external-docs": { + "$ref": "#/$defs/extensible", + "properties": { + "description": { + "type": "string" + }, + "url": { + "format": "uri-reference", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object", + "unevaluatedProperties": false + }, + "xml": { + "$ref": "#/$defs/extensible", + "properties": { + "nodeType": { + "type": "string", + "enum": [ + "element", + "attribute", + "text", + "cdata", + "none" + ] + }, + "name": { + "type": "string" + }, + "namespace": { + "format": "iri", + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "deprecated": true + }, + "wrapped": { + "type": "boolean", + "deprecated": true + } + }, + "type": "object", + "dependentSchemas": { + "nodeType": { + "properties": { + "attribute": false, + "wrapped": false + } + } + }, + "unevaluatedProperties": false + } + } +} diff --git a/openapi_schema_validator/shortcuts.py b/openapi_schema_validator/shortcuts.py index ad86277..bb486ba 100644 --- a/openapi_schema_validator/shortcuts.py +++ b/openapi_schema_validator/shortcuts.py @@ -6,14 +6,15 @@ 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._dialects import OAS32_BASE_DIALECT_ID +from openapi_schema_validator.validators import OAS32Validator from openapi_schema_validator.validators import check_openapi_schema def validate( instance: Any, schema: Mapping[str, Any], - cls: type[Validator] = OAS31Validator, + cls: type[Validator] = OAS32Validator, *args: Any, **kwargs: Any ) -> None: @@ -24,11 +25,11 @@ def validate( 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 + # registry, so for OAS dialects 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 + if isinstance(meta_schema, dict) and meta_schema.get("$id") in ( + OAS31_BASE_DIALECT_ID, + OAS32_BASE_DIALECT_ID, ): check_openapi_schema(cls, schema_dict) else: diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index d90d229..f151d28 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -15,6 +15,8 @@ from openapi_schema_validator import _types as oas_types from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_ID from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_METASCHEMA +from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID +from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_METASCHEMA from openapi_schema_validator._dialects import register_openapi_dialect from openapi_schema_validator._specifications import ( REGISTRY as OPENAPI_SPECIFICATIONS, @@ -134,6 +136,20 @@ def _build_oas31_validator() -> Any: ) +def _build_oas32_validator() -> Any: + validator = extend( + OAS31Validator, + {}, + format_checker=oas_format.oas32_format_checker, + ) + return register_openapi_dialect( + validator=validator, + dialect_id=OAS32_BASE_DIALECT_ID, + version_name="oas32", + metaschema=OAS32_BASE_DIALECT_METASCHEMA, + ) + + OAS30Validator = _build_oas30_validator() OAS30StrictValidator = extend( OAS30Validator, @@ -162,13 +178,4 @@ def _build_oas31_validator() -> Any: ) OAS31Validator = _build_oas31_validator() - -# OAS 3.2 uses JSON Schema Draft 2020-12 as its base dialect, same as -# OAS 3.1. The OAS-specific vocabulary differs slightly (e.g. xml keyword -# changes), but since xml is not_implemented in the current validators, -# the behavior is equivalent. -OAS32Validator = extend( - OAS31Validator, - {}, - format_checker=oas_format.oas32_format_checker, -) +OAS32Validator = _build_oas32_validator() diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 8b39f9c..031f1bf 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -4,6 +4,7 @@ from typing import cast import pytest +from jsonschema import SchemaError from jsonschema import ValidationError from jsonschema.exceptions import ( _WrappedReferencingError as WrappedReferencingError, @@ -28,9 +29,12 @@ from openapi_schema_validator import oas30_strict_format_checker from openapi_schema_validator import oas31_format_checker from openapi_schema_validator import oas32_format_checker +from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_ID from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_METASCHEMA +from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID +from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_METASCHEMA from openapi_schema_validator._dialects import register_openapi_dialect -from openapi_schema_validator.validators import OAS31_BASE_DIALECT_ID +from openapi_schema_validator.validators import check_openapi_schema class TestOAS30ValidatorFormatChecker: @@ -1013,7 +1017,7 @@ def test_array_prefixitems_invalid(self, validator_class, value): class TestOAS32ValidatorValidate(TestOAS31ValidatorValidate): - """OAS 3.2 uses the same JSON Schema dialect as 3.1.""" + """OAS 3.2 uses the OAS 3.2 published dialect resources.""" @pytest.fixture def validator_class(self): @@ -1032,6 +1036,9 @@ def test_format_checker_is_distinct_from_oas31(self): def test_validator_shares_oas31_behavior(self): assert OAS32Validator.VALIDATORS == OAS31Validator.VALIDATORS + def test_validator_has_oas32_dialect_metaschema(self): + assert OAS32Validator.META_SCHEMA["$id"] == OAS32_BASE_DIALECT_ID + def test_format_validation_int32(self, validator_class): schema = {"type": "integer", "format": "int32"} validator = validator_class( @@ -1071,6 +1078,29 @@ def test_schema_with_allof(self, validator_class): with pytest.raises(ValidationError): validator.validate({"id": "not-an-integer"}) + def test_check_schema_accepts_oas32_discriminator_default_mapping(self): + schema = { + "type": "object", + "discriminator": { + "propertyName": "kind", + "defaultMapping": "#/components/schemas/Pet", + }, + } + + check_openapi_schema(OAS32Validator, schema) + + def test_oas31_check_schema_rejects_discriminator_default_mapping(self): + schema = { + "type": "object", + "discriminator": { + "propertyName": "kind", + "defaultMapping": "#/components/schemas/Pet", + }, + } + + with pytest.raises(SchemaError): + check_openapi_schema(OAS31Validator, schema) + class TestOAS30StrictValidator: """ @@ -1142,6 +1172,25 @@ def test_oas31_base_dialect_discovery_has_no_deprecation_warning(self): for warning in caught ) + def test_oas32_base_dialect_resolves_to_oas32_validator(self): + schema = {"$schema": OAS32_BASE_DIALECT_ID} + + validator_class = validator_for(schema) + + assert validator_class is OAS32Validator + + def test_oas32_base_dialect_discovery_has_no_deprecation_warning(self): + schema = {"$schema": OAS32_BASE_DIALECT_ID} + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + validator_for(schema) + + assert not any( + issubclass(warning.category, DeprecationWarning) + for warning in caught + ) + def test_oas31_base_dialect_keeps_oas_keyword_behavior(self): schema = { "$schema": OAS31_BASE_DIALECT_ID, @@ -1201,3 +1250,21 @@ def test_openapi_dialect_registration_does_not_replace_validator(self): assert ( validator_for({"$schema": OAS31_BASE_DIALECT_ID}) is OAS31Validator ) + + def test_openapi_oas32_dialect_registration_is_idempotent(self): + register_openapi_dialect( + validator=OAS32Validator, + dialect_id=OAS32_BASE_DIALECT_ID, + version_name="oas32", + metaschema=OAS32_BASE_DIALECT_METASCHEMA, + ) + register_openapi_dialect( + validator=OAS32Validator, + dialect_id=OAS32_BASE_DIALECT_ID, + version_name="oas32", + metaschema=OAS32_BASE_DIALECT_METASCHEMA, + ) + + validator_class = validator_for({"$schema": OAS32_BASE_DIALECT_ID}) + + assert validator_class is OAS32Validator diff --git a/tests/unit/test_shortcut.py b/tests/unit/test_shortcut.py index 95cafe4..e391d75 100644 --- a/tests/unit/test_shortcut.py +++ b/tests/unit/test_shortcut.py @@ -1,7 +1,9 @@ +import inspect from unittest.mock import patch import pytest +from openapi_schema_validator import OAS32Validator from openapi_schema_validator import validate @@ -41,3 +43,16 @@ def test_validate_does_not_fetch_remote_metaschemas(schema): validate({"email": "foo@bar.com"}, schema) urlopen.assert_not_called() + + +def test_validate_defaults_to_oas32_validator(): + signature = inspect.signature(validate) + + assert signature.parameters["cls"].default is OAS32Validator + + +def test_oas32_validate_does_not_fetch_remote_metaschemas(schema): + with patch("urllib.request.urlopen") as urlopen: + validate({"email": "foo@bar.com"}, schema, cls=OAS32Validator) + + urlopen.assert_not_called() From 7095c26536f73d6d8ac68db01faf20525549b699 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 25 Feb 2026 21:46:22 +0000 Subject: [PATCH 2/2] Bound class-level overrised --- openapi_schema_validator/validators.py | 3 +++ tests/integration/test_validators.py | 20 +++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index f151d28..6d82006 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -179,3 +179,6 @@ def _build_oas32_validator() -> Any: OAS31Validator = _build_oas31_validator() OAS32Validator = _build_oas32_validator() + +OAS31Validator.check_schema = classmethod(check_openapi_schema) +OAS32Validator.check_schema = classmethod(check_openapi_schema) diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 031f1bf..65cc864 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -2,6 +2,7 @@ from base64 import b64encode from typing import Any from typing import cast +from unittest.mock import patch import pytest from jsonschema import SchemaError @@ -34,7 +35,6 @@ from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_METASCHEMA from openapi_schema_validator._dialects import register_openapi_dialect -from openapi_schema_validator.validators import check_openapi_schema class TestOAS30ValidatorFormatChecker: @@ -1087,7 +1087,7 @@ def test_check_schema_accepts_oas32_discriminator_default_mapping(self): }, } - check_openapi_schema(OAS32Validator, schema) + OAS32Validator.check_schema(schema) def test_oas31_check_schema_rejects_discriminator_default_mapping(self): schema = { @@ -1099,7 +1099,21 @@ def test_oas31_check_schema_rejects_discriminator_default_mapping(self): } with pytest.raises(SchemaError): - check_openapi_schema(OAS31Validator, schema) + OAS31Validator.check_schema(schema) + + def test_oas32_check_schema_does_not_fetch_remote_metaschemas(self): + schema = { + "type": "object", + "discriminator": { + "propertyName": "kind", + "defaultMapping": "#/components/schemas/Pet", + }, + } + + with patch("urllib.request.urlopen") as urlopen: + OAS32Validator.check_schema(schema) + + urlopen.assert_not_called() class TestOAS30StrictValidator: