From 859c9d6db7e53a85afb9925a2f35c12d0bd5ed5c Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 25 Feb 2026 20:19:08 +0000 Subject: [PATCH] OpenAPI 3.2 support --- README.rst | 7 +- docs/cli.rst | 4 +- docs/index.rst | 7 +- docs/python.rst | 9 +- openapi_spec_validator/__init__.py | 4 + openapi_spec_validator/__main__.py | 20 +- .../resources/schemas/v3.2/schema.json | 1684 +++++++++++++++++ openapi_spec_validator/schemas/__init__.py | 8 +- openapi_spec_validator/shortcuts.py | 2 + openapi_spec_validator/validation/__init__.py | 17 +- openapi_spec_validator/validation/keywords.py | 44 +- .../validation/validators.py | 22 + openapi_spec_validator/versions/consts.py | 8 +- tests/integration/data/v3.2/petstore.yaml | 115 ++ tests/integration/test_main.py | 22 + tests/integration/test_shortcuts.py | 26 + tests/integration/test_versions.py | 2 + tests/integration/validation/test_dialect.py | 41 +- .../integration/validation/test_validators.py | 76 + 19 files changed, 2093 insertions(+), 25 deletions(-) create mode 100644 openapi_spec_validator/resources/schemas/v3.2/schema.json create mode 100644 tests/integration/data/v3.2/petstore.yaml diff --git a/README.rst b/README.rst index 8b19251..149a472 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,8 @@ OpenAPI Spec Validator is a CLI, pre-commit hook and python package that validat against the `OpenAPI 2.0 (aka Swagger) `__, `OpenAPI 3.0 `__ -and `OpenAPI 3.1 `__ +`OpenAPI 3.1 `__ +and `OpenAPI 3.2 `__ specification. The validator aims to check for full compliance with the Specification. @@ -119,9 +120,9 @@ Related projects ################ * `openapi-core `__ - Python library that adds client-side and server-side support for the OpenAPI v3.0 and OpenAPI v3.1 specification. + Python library that adds client-side and server-side support for the OpenAPI v3.0, OpenAPI v3.1 and OpenAPI v3.2 specification. * `openapi-schema-validator `__ - Python library that validates schema against the OpenAPI Schema Specification v3.0 and OpenAPI Schema Specification v3.1. + Python library that validates schema against the OpenAPI Schema Specification v3.0, OpenAPI Schema Specification v3.1 and OpenAPI Schema Specification v3.2. License ####### diff --git a/docs/cli.rst b/docs/cli.rst index ee80ab2..88a4e89 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -45,7 +45,7 @@ CLI (Command Line Interface) usage: openapi-spec-validator [-h] [--subschema-errors {best-match,all}] [--validation-errors {first,all}] - [--errors {best-match,all}] [--schema {detect,2.0,3.0,3.1}] + [--errors {best-match,all}] [--schema {detect,2.0,3.0,3.1,3.2}] [--version] file [file ...] positional arguments: @@ -61,7 +61,7 @@ CLI (Command Line Interface) use "all" to get all validation errors. --errors {best-match,all}, --error {best-match,all} Deprecated alias for --subschema-errors. - --schema {detect,2.0,3.0,3.1} + --schema {detect,2.0,3.0,3.1,3.2} OpenAPI schema version (default: detect). --version show program's version number and exit diff --git a/docs/index.rst b/docs/index.rst index 889f4ec..adce589 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,8 @@ OpenAPI Spec Validator is a CLI, pre-commit hook and python package that validat against the `OpenAPI 2.0 (aka Swagger) `__, `OpenAPI 3.0 `__ -and `OpenAPI 3.1 `__ +`OpenAPI 3.1 `__ +and `OpenAPI 3.2 `__ specification. The validator aims to check for full compliance with the Specification. Installation @@ -111,9 +112,9 @@ Related projects ---------------- * `openapi-core `__ - Python library that adds client-side and server-side support for the OpenAPI v3.0 and OpenAPI v3.1 specification. + Python library that adds client-side and server-side support for the OpenAPI v3.0, OpenAPI v3.1 and OpenAPI v3.2 specification. * `openapi-schema-validator `__ - Python library that validates schema against the OpenAPI Schema Specification v3.0 and OpenAPI Schema Specification v3.1. + Python library that validates schema against the OpenAPI Schema Specification v3.0, OpenAPI Schema Specification v3.1 and OpenAPI Schema Specification v3.2. License ------- diff --git a/docs/python.rst b/docs/python.rst index 7bb05ae..88eaf97 100644 --- a/docs/python.rst +++ b/docs/python.rst @@ -41,13 +41,14 @@ In order to explicitly validate a: * Swagger / OpenAPI 2.0 spec, import ``OpenAPIV2SpecValidator`` * OpenAPI 3.0 spec, import ``OpenAPIV30SpecValidator`` -* OpenAPI 3.1 spec, import ``OpenAPIV31SpecValidator`` +* OpenAPI 3.1 spec, import ``OpenAPIV31SpecValidator`` +* OpenAPI 3.2 spec, import ``OpenAPIV32SpecValidator`` and pass the validator class to ``validate`` or ``validate_url`` function: .. code:: python - validate(spec_dict, cls=OpenAPIV31SpecValidator) + validate(spec_dict, cls=OpenAPIV32SpecValidator) You can also explicitly import ``OpenAPIV3SpecValidator`` which is a shortcut to the latest v3 release. @@ -55,6 +56,6 @@ If you want to iterate through validation errors: .. code:: python - from openapi_spec_validator import OpenAPIV31SpecValidator + from openapi_spec_validator import OpenAPIV32SpecValidator - errors_iterator = OpenAPIV31SpecValidator(spec).iter_errors() + errors_iterator = OpenAPIV32SpecValidator(spec).iter_errors() diff --git a/openapi_spec_validator/__init__.py b/openapi_spec_validator/__init__.py index 6a0b1dc..af7620a 100644 --- a/openapi_spec_validator/__init__.py +++ b/openapi_spec_validator/__init__.py @@ -8,10 +8,12 @@ from openapi_spec_validator.validation import OpenAPIV3SpecValidator from openapi_spec_validator.validation import OpenAPIV30SpecValidator from openapi_spec_validator.validation import OpenAPIV31SpecValidator +from openapi_spec_validator.validation import OpenAPIV32SpecValidator from openapi_spec_validator.validation import openapi_v2_spec_validator from openapi_spec_validator.validation import openapi_v3_spec_validator from openapi_spec_validator.validation import openapi_v30_spec_validator from openapi_spec_validator.validation import openapi_v31_spec_validator +from openapi_spec_validator.validation import openapi_v32_spec_validator __author__ = "Artur Maciag" __email__ = "maciag.artur@gmail.com" @@ -24,10 +26,12 @@ "openapi_v3_spec_validator", "openapi_v30_spec_validator", "openapi_v31_spec_validator", + "openapi_v32_spec_validator", "OpenAPIV2SpecValidator", "OpenAPIV3SpecValidator", "OpenAPIV30SpecValidator", "OpenAPIV31SpecValidator", + "OpenAPIV32SpecValidator", "validate", "validate_url", "validate_spec", diff --git a/openapi_spec_validator/__main__.py b/openapi_spec_validator/__main__.py index 9b7bdeb..f5ff1f2 100644 --- a/openapi_spec_validator/__main__.py +++ b/openapi_spec_validator/__main__.py @@ -15,6 +15,7 @@ from openapi_spec_validator.validation import OpenAPIV2SpecValidator from openapi_spec_validator.validation import OpenAPIV30SpecValidator from openapi_spec_validator.validation import OpenAPIV31SpecValidator +from openapi_spec_validator.validation import OpenAPIV32SpecValidator from openapi_spec_validator.validation import SpecValidator logger = logging.getLogger(__name__) @@ -55,8 +56,8 @@ def print_validationerror( print("## " + str(best_match(exc.context))) if len(exc.context) > 1: print( - f"\n({len(exc.context) - 1} more subschemas errors,", - "use --subschema-errors=all to see them.)", + f"\n({len(exc.context) - 1} more subschemas errors, " + "use --subschema-errors=all to see them.)" ) @@ -101,9 +102,18 @@ def main(args: Sequence[str] | None = None) -> None: parser.add_argument( "--schema", type=str, - choices=["detect", "2.0", "3.0", "3.1", "3.0.0", "3.1.0"], + choices=[ + "detect", + "2.0", + "3.0", + "3.1", + "3.2", + "3.0.0", + "3.1.0", + "3.2.0", + ], default="detect", - metavar="{detect,2.0,3.0,3.1}", + metavar="{detect,2.0,3.0,3.1,3.2}", help="OpenAPI schema version (default: detect).", ) parser.add_argument( @@ -149,9 +159,11 @@ def main(args: Sequence[str] | None = None) -> None: "2.0": OpenAPIV2SpecValidator, "3.0": OpenAPIV30SpecValidator, "3.1": OpenAPIV31SpecValidator, + "3.2": OpenAPIV32SpecValidator, # backward compatibility "3.0.0": OpenAPIV30SpecValidator, "3.1.0": OpenAPIV31SpecValidator, + "3.2.0": OpenAPIV32SpecValidator, } validator_cls = validators[args_parsed.schema] diff --git a/openapi_spec_validator/resources/schemas/v3.2/schema.json b/openapi_spec_validator/resources/schemas/v3.2/schema.json new file mode 100644 index 0000000..95ab03f --- /dev/null +++ b/openapi_spec_validator/resources/schemas/v3.2/schema.json @@ -0,0 +1,1684 @@ +{ + "$id": "https://spec.openapis.org/oas/3.2/schema/2025-11-23", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of OpenAPI v3.2.x Documents without Schema Object validation", + "type": "object", + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.2\\.\\d+(-.+)?$" + }, + "$self": { + "type": "string", + "format": "uri-reference", + "$comment": "MUST NOT contain a fragment", + "pattern": "^[^#]*$" + }, + "info": { + "$ref": "#/$defs/info" + }, + "jsonSchemaDialect": { + "type": "string", + "format": "uri-reference", + "default": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + }, + "default": [ + { + "url": "/" + } + ] + }, + "paths": { + "$ref": "#/$defs/paths" + }, + "webhooks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "components": { + "$ref": "#/$defs/components" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/$defs/tag" + } + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "openapi", + "info" + ], + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "components" + ] + }, + { + "required": [ + "webhooks" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "info": { + "$comment": "https://spec.openapis.org/oas/v3.2#info-object", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/$defs/contact" + }, + "license": { + "$ref": "#/$defs/license" + }, + "version": { + "type": "string" + } + }, + "required": [ + "title", + "version" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "contact": { + "$comment": "https://spec.openapis.org/oas/v3.2#contact-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "license": { + "$comment": "https://spec.openapis.org/oas/v3.2#license-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "name" + ], + "dependentSchemas": { + "identifier": { + "not": { + "required": [ + "url" + ] + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server": { + "$comment": "https://spec.openapis.org/oas/v3.2#server-object", + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server-variable" + } + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server-variable": { + "$comment": "https://spec.openapis.org/oas/v3.2#server-variable-object", + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "default" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "components": { + "$comment": "https://spec.openapis.org/oas/v3.2#components-object", + "type": "object", + "properties": { + "schemas": { + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta" + } + }, + "responses": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/response-or-reference" + } + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + }, + "requestBodies": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/request-body-or-reference" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/security-scheme-or-reference" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "pathItems": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "mediaTypes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type-or-reference" + } + } + }, + "patternProperties": { + "^(?:schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems|mediaTypes)$": { + "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", + "propertyNames": { + "pattern": "^[a-zA-Z0-9._-]+$" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "paths": { + "$comment": "https://spec.openapis.org/oas/v3.2#paths-object", + "type": "object", + "patternProperties": { + "^/": { + "$ref": "#/$defs/path-item" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item": { + "$comment": "https://spec.openapis.org/oas/v3.2#path-item-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + }, + "parameters": { + "$ref": "#/$defs/parameters" + }, + "additionalOperations": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/operation" + }, + "propertyNames": { + "$comment": "RFC9110 restricts methods to \"1*tchar\" in ABNF", + "pattern": "^[a-zA-Z0-9!#$%&'*+.^_`|~-]+$", + "not": { + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "OPTIONS", + "HEAD", + "PATCH", + "TRACE", + "QUERY" + ] + } + } + }, + "get": { + "$ref": "#/$defs/operation" + }, + "put": { + "$ref": "#/$defs/operation" + }, + "post": { + "$ref": "#/$defs/operation" + }, + "delete": { + "$ref": "#/$defs/operation" + }, + "options": { + "$ref": "#/$defs/operation" + }, + "head": { + "$ref": "#/$defs/operation" + }, + "patch": { + "$ref": "#/$defs/operation" + }, + "trace": { + "$ref": "#/$defs/operation" + }, + "query": { + "$ref": "#/$defs/operation" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "operation": { + "$comment": "https://spec.openapis.org/oas/v3.2#operation-object", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/parameters" + }, + "requestBody": { + "$ref": "#/$defs/request-body-or-reference" + }, + "responses": { + "$ref": "#/$defs/responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "external-documentation": { + "$comment": "https://spec.openapis.org/oas/v3.2#external-documentation-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + }, + "not": { + "allOf": [ + { + "contains": { + "type": "object", + "properties": { + "in": { + "const": "query" + } + }, + "required": [ + "in" + ] + } + }, + { + "contains": { + "type": "object", + "properties": { + "in": { + "const": "querystring" + } + }, + "required": [ + "in" + ] + } + } + ] + }, + "contains": { + "type": "object", + "properties": { + "in": { + "const": "querystring" + } + }, + "required": [ + "in" + ] + }, + "minContains": 0, + "maxContains": 1 + }, + "parameter": { + "$comment": "https://spec.openapis.org/oas/v3.2#parameter-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "querystring", + "header", + "path", + "cookie" + ] + }, + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "required": [ + "name", + "in" + ], + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/specification-extensions" + }, + { + "if": { + "properties": { + "in": { + "const": "query" + } + } + }, + "then": { + "properties": { + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "in": { + "const": "querystring" + } + } + }, + "then": { + "required": [ + "content" + ] + } + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" + } + ], + "$defs": { + "styles-for-path": { + "if": { + "properties": { + "in": { + "const": "path" + } + } + }, + "then": { + "properties": { + "name": { + "pattern": "^[^{}]+$" + }, + "style": { + "default": "simple", + "enum": [ + "matrix", + "label", + "simple" + ] + }, + "required": { + "const": true + }, + "explode": { + "default": false + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "required": [ + "required" + ] + } + }, + "styles-for-header": { + "if": { + "properties": { + "in": { + "const": "header" + } + } + }, + "then": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + }, + "explode": { + "default": false + } + } + } + }, + "styles-for-query": { + "if": { + "properties": { + "in": { + "const": "query" + } + } + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "$ref": "#/$defs/explode-for-form" + } + }, + "styles-for-cookie": { + "if": { + "properties": { + "in": { + "const": "cookie" + } + } + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "cookie" + ] + }, + "explode": { + "default": true + } + }, + "if": { + "properties": { + "style": { + "const": "form" + } + } + }, + "then": { + "properties": { + "allowReserved": { + "type": "boolean", + "default": false + } + } + } + } + } + } + } + }, + "unevaluatedProperties": false + }, + "parameter-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/parameter" + } + }, + "request-body": { + "$comment": "https://spec.openapis.org/oas/v3.2#request-body-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "content": { + "$ref": "#/$defs/content" + }, + "required": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "content" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "request-body-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/request-body" + } + }, + "content": { + "$comment": "https://spec.openapis.org/oas/v3.2#fixed-fields-10", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type-or-reference" + }, + "propertyNames": { + "format": "media-range" + } + }, + "media-type": { + "$comment": "https://spec.openapis.org/oas/v3.2#media-type-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "itemSchema": { + "$dynamicRef": "#meta" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + }, + "prefixEncoding": { + "type": "array", + "items": { + "$ref": "#/$defs/encoding" + } + }, + "itemEncoding": { + "$ref": "#/$defs/encoding" + } + }, + "dependentSchemas": { + "encoding": { + "properties": { + "prefixEncoding": false, + "itemEncoding": false + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/specification-extensions" + } + ], + "unevaluatedProperties": false + }, + "media-type-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/media-type" + } + }, + "encoding": { + "$comment": "https://spec.openapis.org/oas/v3.2#encoding-object", + "type": "object", + "properties": { + "contentType": { + "type": "string", + "format": "media-range" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + }, + "prefixEncoding": { + "type": "array", + "items": { + "$ref": "#/$defs/encoding" + } + }, + "itemEncoding": { + "$ref": "#/$defs/encoding" + } + }, + "dependentSchemas": { + "encoding": { + "properties": { + "prefixEncoding": false, + "itemEncoding": false + } + }, + "style": { + "properties": { + "allowReserved": { + "default": false + } + }, + "$ref": "#/$defs/explode-for-form" + }, + "explode": { + "properties": { + "style": { + "default": "form" + }, + "allowReserved": { + "default": false + } + } + }, + "allowReserved": { + "properties": { + "style": { + "default": "form" + } + }, + "$ref": "#/$defs/explode-for-form" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "responses": { + "$comment": "https://spec.openapis.org/oas/v3.2#responses-object", + "type": "object", + "properties": { + "default": { + "$ref": "#/$defs/response-or-reference" + } + }, + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": { + "$ref": "#/$defs/response-or-reference" + } + }, + "minProperties": 1, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "if": { + "$comment": "either default, or at least one response code property must exist", + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": false + } + }, + "then": { + "required": [ + "default" + ] + } + }, + "response": { + "$comment": "https://spec.openapis.org/oas/v3.2#response-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "content": { + "$ref": "#/$defs/content" + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/response" + } + }, + "callbacks": { + "$comment": "https://spec.openapis.org/oas/v3.2#callback-object", + "type": "object", + "$ref": "#/$defs/specification-extensions", + "additionalProperties": { + "$ref": "#/$defs/path-item" + } + }, + "callbacks-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/callbacks" + } + }, + "example": { + "$comment": "https://spec.openapis.org/oas/v3.2#example-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dataValue": true, + "serializedValue": { + "type": "string" + }, + "value": true, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "allOf": [ + { + "not": { + "required": [ + "value", + "externalValue" + ] + } + }, + { + "not": { + "required": [ + "value", + "dataValue" + ] + } + }, + { + "not": { + "required": [ + "value", + "serializedValue" + ] + } + }, + { + "not": { + "required": [ + "serializedValue", + "externalValue" + ] + } + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "example-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/example" + } + }, + "link": { + "$comment": "https://spec.openapis.org/oas/v3.2#link-object", + "type": "object", + "properties": { + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/map-of-strings" + }, + "requestBody": true, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/$defs/server" + } + }, + "oneOf": [ + { + "required": [ + "operationRef" + ] + }, + { + "required": [ + "operationId" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "link-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/link" + } + }, + "header": { + "$comment": "https://spec.openapis.org/oas/v3.2#header-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + }, + "explode": { + "default": false, + "type": "boolean" + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/specification-extensions" + } + ], + "unevaluatedProperties": false + }, + "header-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/header" + } + }, + "tag": { + "$comment": "https://spec.openapis.org/oas/v3.2#tag-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "parent": { + "type": "string" + }, + "kind": { + "type": "string" + } + }, + "required": [ + "name" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "reference": { + "$comment": "https://spec.openapis.org/oas/v3.2#reference-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "schema": { + "$comment": "https://spec.openapis.org/oas/v3.2#schema-object", + "$dynamicAnchor": "meta", + "type": [ + "object", + "boolean" + ] + }, + "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.2#security-scheme-object", + "type": "object", + "properties": { + "type": { + "enum": [ + "apiKey", + "http", + "mutualTLS", + "oauth2", + "openIdConnect" + ] + }, + "description": { + "type": "string" + }, + "deprecated": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-apikey" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oauth2" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oidc" + } + ], + "unevaluatedProperties": false, + "$defs": { + "type-apikey": { + "if": { + "properties": { + "type": { + "const": "apiKey" + } + } + }, + "then": { + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "cookie" + ] + } + }, + "required": [ + "name", + "in" + ] + } + }, + "type-http": { + "if": { + "properties": { + "type": { + "const": "http" + } + } + }, + "then": { + "properties": { + "scheme": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + } + }, + "type-http-bearer": { + "if": { + "properties": { + "type": { + "const": "http" + }, + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + }, + "required": [ + "type", + "scheme" + ] + }, + "then": { + "properties": { + "bearerFormat": { + "type": "string" + } + } + } + }, + "type-oauth2": { + "if": { + "properties": { + "type": { + "const": "oauth2" + } + } + }, + "then": { + "properties": { + "flows": { + "$ref": "#/$defs/oauth-flows" + }, + "oauth2MetadataUrl": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "flows" + ] + } + }, + "type-oidc": { + "if": { + "properties": { + "type": { + "const": "openIdConnect" + } + } + }, + "then": { + "properties": { + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "openIdConnectUrl" + ] + } + } + } + }, + "security-scheme-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/security-scheme" + } + }, + "oauth-flows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/$defs/oauth-flows/$defs/implicit" + }, + "password": { + "$ref": "#/$defs/oauth-flows/$defs/password" + }, + "clientCredentials": { + "$ref": "#/$defs/oauth-flows/$defs/client-credentials" + }, + "authorizationCode": { + "$ref": "#/$defs/oauth-flows/$defs/authorization-code" + }, + "deviceAuthorization": { + "$ref": "#/$defs/oauth-flows/$defs/device-authorization" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "implicit": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "password": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "client-credentials": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "authorization-code": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "device-authorization": { + "type": "object", + "properties": { + "deviceAuthorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "deviceAuthorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + } + } + }, + "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.2#security-requirement-object", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.2#specification-extensions", + "patternProperties": { + "^x-": true + } + }, + "examples": { + "properties": { + "example": true, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + } + }, + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "map-of-strings": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "explode-for-form": { + "$comment": "for encoding objects, and query and cookie parameters, style=form is the default", + "if": { + "properties": { + "style": { + "const": "form" + } + } + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } +} diff --git a/openapi_spec_validator/schemas/__init__.py b/openapi_spec_validator/schemas/__init__.py index e1147bc..ef4cc4d 100644 --- a/openapi_spec_validator/schemas/__init__.py +++ b/openapi_spec_validator/schemas/__init__.py @@ -8,23 +8,27 @@ from openapi_spec_validator.schemas.utils import get_schema_content -__all__ = ["schema_v2", "schema_v3", "schema_v30", "schema_v31"] +__all__ = ["schema_v2", "schema_v3", "schema_v30", "schema_v31", "schema_v32"] get_schema_content_v2 = partial(get_schema_content, "2.0") get_schema_content_v30 = partial(get_schema_content, "3.0") get_schema_content_v31 = partial(get_schema_content, "3.1") +get_schema_content_v32 = partial(get_schema_content, "3.2") schema_v2 = Proxy(get_schema_content_v2) schema_v30 = Proxy(get_schema_content_v30) schema_v31 = Proxy(get_schema_content_v31) +schema_v32 = Proxy(get_schema_content_v32) # alias to the latest v3 version -schema_v3 = schema_v31 +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) diff --git a/openapi_spec_validator/shortcuts.py b/openapi_spec_validator/shortcuts.py index 884d079..3b32ee1 100644 --- a/openapi_spec_validator/shortcuts.py +++ b/openapi_spec_validator/shortcuts.py @@ -10,6 +10,7 @@ from openapi_spec_validator.validation import OpenAPIV2SpecValidator from openapi_spec_validator.validation import OpenAPIV30SpecValidator from openapi_spec_validator.validation import OpenAPIV31SpecValidator +from openapi_spec_validator.validation import OpenAPIV32SpecValidator from openapi_spec_validator.validation.exceptions import ValidatorDetectError from openapi_spec_validator.validation.protocols import SupportsValidation from openapi_spec_validator.validation.types import SpecValidatorType @@ -23,6 +24,7 @@ versions.OPENAPIV2: OpenAPIV2SpecValidator, versions.OPENAPIV30: OpenAPIV30SpecValidator, versions.OPENAPIV31: OpenAPIV31SpecValidator, + versions.OPENAPIV32: OpenAPIV32SpecValidator, } diff --git a/openapi_spec_validator/validation/__init__.py b/openapi_spec_validator/validation/__init__.py index d30e991..ad15027 100644 --- a/openapi_spec_validator/validation/__init__.py +++ b/openapi_spec_validator/validation/__init__.py @@ -7,6 +7,9 @@ from openapi_spec_validator.validation.validators import ( OpenAPIV31SpecValidator, ) +from openapi_spec_validator.validation.validators import ( + OpenAPIV32SpecValidator, +) from openapi_spec_validator.validation.validators import SpecValidator __all__ = [ @@ -14,11 +17,13 @@ "openapi_v3_spec_validator", "openapi_v30_spec_validator", "openapi_v31_spec_validator", + "openapi_v32_spec_validator", "openapi_spec_validator_proxy", "OpenAPIV2SpecValidator", "OpenAPIV3SpecValidator", "OpenAPIV30SpecValidator", "OpenAPIV31SpecValidator", + "OpenAPIV32SpecValidator", "SpecValidator", ] @@ -43,9 +48,16 @@ use="OpenAPIV31SpecValidator", ) +# v3.2 spec +openapi_v32_spec_validator = SpecValidatorProxy( + OpenAPIV32SpecValidator, + deprecated="openapi_v32_spec_validator", + use="OpenAPIV32SpecValidator", +) + # alias to the latest v3 version -openapi_v3_spec_validator = openapi_v31_spec_validator -OpenAPIV3SpecValidator = OpenAPIV31SpecValidator +openapi_v3_spec_validator = openapi_v32_spec_validator +OpenAPIV3SpecValidator = OpenAPIV32SpecValidator # detect version spec openapi_spec_validator_proxy = DetectValidatorProxy( @@ -53,5 +65,6 @@ ("swagger", "2.0"): openapi_v2_spec_validator, ("openapi", "3.0"): openapi_v30_spec_validator, ("openapi", "3.1"): openapi_v31_spec_validator, + ("openapi", "3.2"): openapi_v32_spec_validator, }, ) diff --git a/openapi_spec_validator/validation/keywords.py b/openapi_spec_validator/validation/keywords.py index 4678f62..5e13070 100644 --- a/openapi_spec_validator/validation/keywords.py +++ b/openapi_spec_validator/validation/keywords.py @@ -16,8 +16,10 @@ from jsonschema_path.paths import SchemaPath from openapi_schema_validator import oas30_format_checker from openapi_schema_validator import oas31_format_checker +from openapi_schema_validator import oas32_format_checker from openapi_schema_validator.validators import OAS30Validator from openapi_schema_validator.validators import OAS31Validator +from openapi_schema_validator.validators import OAS32Validator from openapi_schema_validator.validators import check_openapi_schema from openapi_spec_validator.validation.exceptions import ( @@ -38,6 +40,9 @@ ) OAS31_BASE_DIALECT_URI = "https://spec.openapis.org/oas/3.1/dialect/base" +# OAS 3.2 spec text mentions the 3.1 dialect URI by mistake. +# Maintainer clarification: https://github.com/OAI/OpenAPI-Specification/discussions/5223 +OAS32_BASE_DIALECT_URI = "https://spec.openapis.org/oas/3.2/dialect/2025-09-17" class KeywordValidator: @@ -71,6 +76,11 @@ class OpenAPIV31ValueValidator(ValueValidator): value_validator_format_checker = oas31_format_checker +class OpenAPIV32ValueValidator(ValueValidator): + value_validator_cls = OAS32Validator + value_validator_format_checker = oas32_format_checker + + class SchemaValidator(KeywordValidator): def __init__(self, registry: "KeywordValidatorRegistry"): super().__init__(registry) @@ -244,6 +254,9 @@ def __call__( class OpenAPIV31SchemaValidator(SchemaValidator): + default_jsonschema_dialect_id = OAS31_BASE_DIALECT_URI + schema_validator_format_checker = oas31_format_checker + def __init__(self, registry: "KeywordValidatorRegistry"): super().__init__(registry) self._default_jsonschema_dialect_id: str | None = None @@ -266,7 +279,7 @@ def _get_schema_checker( return partial( check_openapi_schema, validator_cls, - format_checker=oas31_format_checker, + format_checker=self.schema_validator_format_checker, ) def _get_schema_dialect_id( @@ -321,7 +334,7 @@ def _get_default_jsonschema_dialect_id(self, schema: SchemaPath) -> str: spec_root = self._get_spec_root(schema) dialect_id = (spec_root / "jsonSchemaDialect").read_str( - default=OAS31_BASE_DIALECT_URI + default=self.default_jsonschema_dialect_id ) self._default_jsonschema_dialect_id = dialect_id @@ -332,6 +345,18 @@ def _get_spec_root(self, schema: SchemaPath) -> SchemaPath: return schema._clone_with_parts(()) +class OpenAPIV32SchemaValidator(OpenAPIV31SchemaValidator): + default_jsonschema_dialect_id = OAS32_BASE_DIALECT_URI + schema_validator_format_checker = oas32_format_checker + + def _get_validator_class_for_dialect( + self, dialect_id: str + ) -> type[Validator] | None: + if dialect_id == self.default_jsonschema_dialect_id: + return cast(type[Validator], OAS32Validator) + return super()._get_validator_class_for_dialect(dialect_id) + + class SchemasValidator(KeywordValidator): @property def schema_validator(self) -> SchemaValidator: @@ -381,7 +406,7 @@ def __call__(self, parameters: SchemaPath) -> Iterator[ValidationError]: key = (parameter["name"], parameter["in"]) if key in seen: yield ParameterDuplicateError( - f"Duplicate parameter `{parameter['name']}`" + f"Duplicate parameter '{parameter['name']}'" ) seen.add(key) @@ -543,6 +568,7 @@ class PathValidator(KeywordValidator): "head", "patch", "trace", + "query", ] @property @@ -564,6 +590,18 @@ def __call__( for field_name, operation in path_item.items(): assert isinstance(field_name, str) if field_name not in self.OPERATIONS: + if field_name == "additionalOperations": + for ( + operation_name, + additional_operation, + ) in operation.items(): + assert isinstance(operation_name, str) + yield from self.operation_validator( + url, + operation_name, + additional_operation, + parameters, + ) continue yield from self.operation_validator( diff --git a/openapi_spec_validator/validation/validators.py b/openapi_spec_validator/validation/validators.py index 80a76ae..df576f9 100644 --- a/openapi_spec_validator/validation/validators.py +++ b/openapi_spec_validator/validation/validators.py @@ -15,6 +15,7 @@ from openapi_spec_validator.schemas import openapi_v2_schema_validator from openapi_spec_validator.schemas import openapi_v30_schema_validator from openapi_spec_validator.schemas import openapi_v31_schema_validator +from openapi_spec_validator.schemas import openapi_v32_schema_validator from openapi_spec_validator.schemas.types import AnySchema from openapi_spec_validator.validation import keywords from openapi_spec_validator.validation.decorators import unwraps_iter @@ -148,3 +149,24 @@ class OpenAPIV31SpecValidator(SpecValidator): "schemas": keywords.SchemasValidator, } root_keywords = ["paths", "components"] + + +class OpenAPIV32SpecValidator(SpecValidator): + schema_validator = openapi_v32_schema_validator + keyword_validators = { + "__root__": keywords.RootValidator, + "components": keywords.ComponentsValidator, + "content": keywords.ContentValidator, + "default": keywords.OpenAPIV32ValueValidator, + "mediaType": keywords.MediaTypeValidator, + "operation": keywords.OperationValidator, + "parameter": keywords.ParameterValidator, + "parameters": keywords.ParametersValidator, + "paths": keywords.PathsValidator, + "path": keywords.PathValidator, + "response": keywords.OpenAPIV3ResponseValidator, + "responses": keywords.ResponsesValidator, + "schema": keywords.OpenAPIV32SchemaValidator, + "schemas": keywords.SchemasValidator, + } + root_keywords = ["paths", "components"] diff --git a/openapi_spec_validator/versions/consts.py b/openapi_spec_validator/versions/consts.py index e864595..8734958 100644 --- a/openapi_spec_validator/versions/consts.py +++ b/openapi_spec_validator/versions/consts.py @@ -18,4 +18,10 @@ minor="1", ) -VERSIONS: list[SpecVersion] = [OPENAPIV2, OPENAPIV30, OPENAPIV31] +OPENAPIV32 = SpecVersion( + keyword="openapi", + major="3", + minor="2", +) + +VERSIONS: list[SpecVersion] = [OPENAPIV2, OPENAPIV30, OPENAPIV31, OPENAPIV32] diff --git a/tests/integration/data/v3.2/petstore.yaml b/tests/integration/data/v3.2/petstore.yaml new file mode 100644 index 0000000..f67f5e7 --- /dev/null +++ b/tests/integration/data/v3.2/petstore.yaml @@ -0,0 +1,115 @@ +openapi: "3.2.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT License + identifier: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + 200: + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + "201": + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + $ref: "#/components/pathItems/PetPath" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + $ref: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + pathItems: + PetPath: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + "200": + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 1ce4e41..e95e4c0 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -38,6 +38,28 @@ def test_schema_v31(capsys): assert "./tests/integration/data/v3.1/petstore.yaml: OK\n" in out +def test_schema_v32_detect(capsys): + """Test schema v3.2 is detected""" + testargs = ["./tests/integration/data/v3.2/petstore.yaml"] + main(testargs) + out, err = capsys.readouterr() + assert not err + assert "./tests/integration/data/v3.2/petstore.yaml: OK\n" in out + + +def test_schema_v32(capsys): + """No errors when calling proper v3.2 file.""" + testargs = [ + "--schema", + "3.2.0", + "./tests/integration/data/v3.2/petstore.yaml", + ] + main(testargs) + out, err = capsys.readouterr() + assert not err + assert "./tests/integration/data/v3.2/petstore.yaml: OK\n" in out + + def test_schema_v30(capsys): """No errors when calling proper v3.0 file.""" testargs = [ diff --git a/tests/integration/test_shortcuts.py b/tests/integration/test_shortcuts.py index f6939c6..cb79007 100644 --- a/tests/integration/test_shortcuts.py +++ b/tests/integration/test_shortcuts.py @@ -1,9 +1,12 @@ import pytest from openapi_spec_validator import OpenAPIV2SpecValidator +from openapi_spec_validator import OpenAPIV3SpecValidator from openapi_spec_validator import OpenAPIV30SpecValidator +from openapi_spec_validator import OpenAPIV32SpecValidator from openapi_spec_validator import openapi_v2_spec_validator from openapi_spec_validator import openapi_v30_spec_validator +from openapi_spec_validator import openapi_v32_spec_validator from openapi_spec_validator import validate from openapi_spec_validator import validate_spec from openapi_spec_validator import validate_spec_url @@ -108,6 +111,29 @@ def test_failed(self, factory, spec_file): validate_spec(spec, validator=openapi_v30_spec_validator) +class TestLocalValidatev32Spec: + LOCAL_SOURCE_DIRECTORY = "data/v3.2/" + + def local_test_suite_file_path(self, test_file): + return f"{self.LOCAL_SOURCE_DIRECTORY}{test_file}" + + @pytest.mark.parametrize( + "spec_file", + [ + "petstore.yaml", + ], + ) + def test_valid(self, factory, spec_file): + spec_path = self.local_test_suite_file_path(spec_file) + spec = factory.spec_from_file(spec_path) + + validate(spec) + validate(spec, cls=OpenAPIV3SpecValidator) + validate(spec, cls=OpenAPIV32SpecValidator) + with pytest.warns(DeprecationWarning): + validate_spec(spec, validator=openapi_v32_spec_validator) + + @pytest.mark.network class TestRemoteValidatev2SpecUrl: REMOTE_SOURCE_URL = ( diff --git a/tests/integration/test_versions.py b/tests/integration/test_versions.py index 891dd50..4545cbc 100644 --- a/tests/integration/test_versions.py +++ b/tests/integration/test_versions.py @@ -31,6 +31,8 @@ def test_invalid(self, keyword, version): ("openapi", "3.0.2", versions.OPENAPIV30), ("openapi", "3.0.3", versions.OPENAPIV30), ("openapi", "3.1.0", versions.OPENAPIV31), + ("openapi", "3.2.0", versions.OPENAPIV32), + ("openapi", "3.2.1", versions.OPENAPIV32), ], ) def test_valid(self, keyword, version, expected): diff --git a/tests/integration/validation/test_dialect.py b/tests/integration/validation/test_dialect.py index 64dee33..73e81d6 100644 --- a/tests/integration/validation/test_dialect.py +++ b/tests/integration/validation/test_dialect.py @@ -1,4 +1,5 @@ from openapi_spec_validator import OpenAPIV31SpecValidator +from openapi_spec_validator import OpenAPIV32SpecValidator from openapi_spec_validator.validation import keywords as validation_keywords from openapi_spec_validator.validation.exceptions import OpenAPIValidationError @@ -6,9 +7,10 @@ def make_spec( component_schema: dict[str, object] | bool, json_schema_dialect: str | None = None, + openapi_version: str = "3.1.0", ) -> dict[str, object]: spec: dict[str, object] = { - "openapi": "3.1.0", + "openapi": openapi_version, "info": { "title": "Test API", "version": "0.0.1", @@ -161,3 +163,40 @@ def counting_validator_for(*args, **kwargs): assert len(errors) == 2 assert all("Unknown JSON Schema dialect" in err.message for err in errors) assert calls["count"] == 1 + + +def test_oas32_default_root_json_schema_dialect_is_honored(): + spec = make_spec( + {"type": "object"}, + json_schema_dialect="https://json-schema.org/draft/2020-12/schema", + openapi_version="3.2.0", + ) + + errors = list(OpenAPIV32SpecValidator(spec).iter_errors()) + + assert errors == [] + + +def test_oas32_uses_default_dialect_when_jsonschema_dialect_is_missing(): + spec = make_spec( + {"type": "object"}, + openapi_version="3.2.0", + ) + + errors = list(OpenAPIV32SpecValidator(spec).iter_errors()) + + assert errors == [] + + +def test_oas32_unknown_dialect_raises_error(): + spec = make_spec( + {"type": "object"}, + json_schema_dialect="https://example.com/custom", + openapi_version="3.2.0", + ) + + errors = list(OpenAPIV32SpecValidator(spec).iter_errors()) + + assert len(errors) == 1 + assert isinstance(errors[0], OpenAPIValidationError) + assert "Unknown JSON Schema dialect" in errors[0].message diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index d5d64b6..bdd3060 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -5,6 +5,7 @@ from openapi_spec_validator import OpenAPIV2SpecValidator from openapi_spec_validator import OpenAPIV30SpecValidator from openapi_spec_validator import OpenAPIV31SpecValidator +from openapi_spec_validator import OpenAPIV32SpecValidator from openapi_spec_validator.validation.exceptions import OpenAPIValidationError @@ -124,6 +125,81 @@ def test_ref_failed(self, factory, spec_file): OpenAPIV30SpecValidator(spec, base_uri=spec_url).validate() +class TestLocalOpenAPIv32Validator: + LOCAL_SOURCE_DIRECTORY = "data/v3.2/" + + def local_test_suite_file_path(self, test_file): + return f"{self.LOCAL_SOURCE_DIRECTORY}{test_file}" + + @pytest.mark.parametrize( + "spec_file", + [ + "petstore.yaml", + ], + ) + def test_valid(self, factory, spec_file): + spec_path = self.local_test_suite_file_path(spec_file) + spec = factory.spec_from_file(spec_path) + spec_url = factory.spec_file_url(spec_path) + validator = OpenAPIV32SpecValidator(spec, base_uri=spec_url) + + validator.validate() + + assert validator.is_valid() + + def test_query_operation_is_semantically_validated(self): + spec = { + "openapi": "3.2.0", + "info": { + "title": "Query API", + "version": "1.0.0", + }, + "paths": { + "/items/{item_id}": { + "query": { + "responses": { + "200": { + "description": "ok", + }, + }, + }, + }, + }, + } + + errors = list(OpenAPIV32SpecValidator(spec).iter_errors()) + + assert len(errors) == 1 + assert "Path parameter 'item_id'" in errors[0].message + + def test_additional_operations_are_semantically_validated(self): + spec = { + "openapi": "3.2.0", + "info": { + "title": "Additional API", + "version": "1.0.0", + }, + "paths": { + "/items/{item_id}": { + "additionalOperations": { + "CUSTOM": { + "responses": { + "200": { + "description": "ok", + }, + }, + }, + }, + }, + }, + } + + errors = list(OpenAPIV32SpecValidator(spec).iter_errors()) + + assert len(errors) == 1 + assert "Path parameter 'item_id'" in errors[0].message + + @pytest.mark.network class TestRemoteOpenAPIv30Validator: REMOTE_SOURCE_URL = (