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
49 changes: 49 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,57 @@ To validate an OpenAPI v3.1 schema:

By default, the latest OpenAPI schema syntax is expected.


Strict vs Pragmatic Validators
=============================

OpenAPI 3.0 has two validator variants with different behaviors for binary format:

**OAS30Validator (default - pragmatic)**
- Accepts Python ``bytes`` for ``type: string`` with ``format: binary``
- More lenient for Python use cases where binary data is common
- Use when validating Python objects directly

**OAS30StrictValidator**
- Follows OAS spec strictly: only accepts ``str`` for ``type: string``
- For ``format: binary``, only accepts base64-encoded strings
- Use when strict spec compliance is required

Comparison Matrix
----------------

.. list-table::
:header-rows: 1
:widths: 35 20 22 23

* - Schema
- Value
- OAS30Validator (default)
- OAS30StrictValidator
* - ``type: string``
- ``"test"`` (str)
- Pass
- Pass
* - ``type: string``
- ``b"test"`` (bytes)
- **Fail**
- **Fail**
* - ``type: string, format: binary``
- ``b"test"`` (bytes)
- Pass
- **Fail**
* - ``type: string, format: binary``
- ``"dGVzdA=="`` (base64)
- Pass
- Pass
* - ``type: string, format: binary``
- ``"test"`` (plain str)
- Pass
- **Fail**

For more details read about `Validation <https://openapi-schema-validator.readthedocs.io/en/latest/validation.html>`__.


Related projects
################
* `openapi-core <https://github.com/python-openapi/openapi-core>`__
Expand Down
63 changes: 63 additions & 0 deletions docs/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,66 @@ OpenAPI 3.0 schema comes with ``readOnly`` and ``writeOnly`` keywords. In order
Traceback (most recent call last):
...
ValidationError: Tried to write read-only property with 23

Strict vs Pragmatic Validators
------------------------------

OpenAPI 3.0 has two validator variants with different behaviors for binary format:

**OAS30Validator (default - pragmatic)**

- Accepts Python ``bytes`` for ``type: string`` with ``format: binary``
- More lenient for Python use cases where binary data is common
- Use when validating Python objects directly

**OAS30StrictValidator**

- Follows OAS spec strictly: only accepts ``str`` for ``type: string``
- For ``format: binary``, only accepts base64-encoded strings
- Use when strict spec compliance is required

Comparison Matrix
~~~~~~~~~~~~~~~~~

.. list-table::
:header-rows: 1
:widths: 35 20 22 23

* - Schema
- Value
- OAS30Validator (default)
- OAS30StrictValidator
* - ``type: string``
- ``"test"`` (str)
- Pass
- Pass
* - ``type: string``
- ``b"test"`` (bytes)
- **Fail**
- **Fail**
* - ``type: string, format: binary``
- ``b"test"`` (bytes)
- Pass
- **Fail**
* - ``type: string, format: binary``
- ``"dGVzdA=="`` (base64)
- Pass
- Pass
* - ``type: string, format: binary``
- ``"test"`` (plain str)
- Pass
- **Fail**

Example usage:

.. code-block:: python

from openapi_schema_validator import OAS30Validator, OAS30StrictValidator

# Pragmatic (default) - accepts bytes for binary format
validator = OAS30Validator({"type": "string", "format": "binary"})
validator.validate(b"binary data") # passes

# Strict - follows spec precisely
validator = OAS30StrictValidator({"type": "string", "format": "binary"})
validator.validate(b"binary data") # raises ValidationError
4 changes: 4 additions & 0 deletions openapi_schema_validator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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
from openapi_schema_validator.shortcuts import validate
from openapi_schema_validator.validators import OAS30ReadValidator
from openapi_schema_validator.validators import OAS30StrictValidator
from openapi_schema_validator.validators import OAS30Validator
from openapi_schema_validator.validators import OAS30WriteValidator
from openapi_schema_validator.validators import OAS31Validator
Expand All @@ -15,9 +17,11 @@
__all__ = [
"validate",
"OAS30ReadValidator",
"OAS30StrictValidator",
"OAS30WriteValidator",
"OAS30Validator",
"oas30_format_checker",
"oas30_strict_format_checker",
"OAS31Validator",
"oas31_format_checker",
]
33 changes: 28 additions & 5 deletions openapi_schema_validator/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,23 @@ def is_double(instance: object) -> bool:
return isinstance(instance, float)


def is_binary(instance: object) -> bool:
if not isinstance(instance, (str, bytes)):
return True
if isinstance(instance, str):
def is_binary_strict(instance: object) -> bool:
# Strict: only accepts base64-encoded strings, not raw bytes
if isinstance(instance, bytes):
return False
if isinstance(instance, str):
try:
b64decode(instance)
return True
except Exception:
return False
return True


def is_binary_pragmatic(instance: object) -> bool:
# Pragmatic: accepts bytes (common in Python) or base64-encoded strings
if isinstance(instance, (str, bytes)):
return True
return True


Expand All @@ -72,10 +84,21 @@ def is_password(instance: object) -> bool:
oas30_format_checker.checks("int64")(is_int64)
oas30_format_checker.checks("float")(is_float)
oas30_format_checker.checks("double")(is_double)
oas30_format_checker.checks("binary")(is_binary)
oas30_format_checker.checks("binary")(is_binary_pragmatic)
oas30_format_checker.checks("byte", (binascii.Error, TypeError))(is_byte)
oas30_format_checker.checks("password")(is_password)

oas30_strict_format_checker = FormatChecker()
oas30_strict_format_checker.checks("int32")(is_int32)
oas30_strict_format_checker.checks("int64")(is_int64)
oas30_strict_format_checker.checks("float")(is_float)
oas30_strict_format_checker.checks("double")(is_double)
oas30_strict_format_checker.checks("binary")(is_binary_strict)
oas30_strict_format_checker.checks("byte", (binascii.Error, TypeError))(
is_byte
)
oas30_strict_format_checker.checks("password")(is_password)

oas31_format_checker = FormatChecker()
oas31_format_checker.checks("int32")(is_int32)
oas31_format_checker.checks("int64")(is_int64)
Expand Down
29 changes: 29 additions & 0 deletions openapi_schema_validator/_keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def type(
instance: Any,
schema: Mapping[str, Any],
) -> Iterator[ValidationError]:
"""Default type validator - allows Python bytes for binary format for pragmatic reasons."""
if instance is None:
# nullable implementation based on OAS 3.0.3
# * nullable is only meaningful if its value is true
Expand All @@ -125,6 +126,34 @@ def type(
return
yield ValidationError("None for not nullable")

# Pragmatic: allow bytes for binary format (common in Python use cases)
if (
data_type == "string"
and schema.get("format") == "binary"
and isinstance(instance, bytes)
):
return

if not validator.is_type(instance, data_type):
data_repr = repr(data_type)
yield ValidationError(f"{instance!r} is not of type {data_repr}")


def strict_type(
validator: Any,
data_type: str,
instance: Any,
schema: Any,
) -> Any:
"""
Strict type validator - follows OAS spec precisely.
Does NOT allow Python bytes for binary format.
"""
if instance is None:
if schema.get("nullable") is True:
return
yield ValidationError("None for not nullable")

if not validator.is_type(instance, data_type):
data_repr = repr(data_type)
yield ValidationError(f"{instance!r} is not of type {data_repr}")
Expand Down
4 changes: 3 additions & 1 deletion openapi_schema_validator/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@


def is_string(checker: Any, instance: Any) -> bool:
return isinstance(instance, (str, bytes))
# Both strict and pragmatic: only accepts str for plain string type
return isinstance(instance, str)


oas30_type_checker = TypeChecker(
Expand All @@ -27,4 +28,5 @@ def is_string(checker: Any, instance: Any) -> bool:
},
),
)

oas31_type_checker = draft202012_type_checker
25 changes: 22 additions & 3 deletions openapi_schema_validator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from jsonschema import _keywords
from jsonschema import _legacy_keywords
from jsonschema.exceptions import ValidationError
from jsonschema.validators import Draft202012Validator
from jsonschema.validators import create
from jsonschema.validators import extend
Expand All @@ -13,6 +14,13 @@
from openapi_schema_validator import _types as oas_types
from openapi_schema_validator._types import oas31_type_checker


def _oas30_id_of(schema: Any) -> str:
if isinstance(schema, dict):
return schema.get("id", "") # type: ignore[no-any-return]
return ""


OAS30_VALIDATORS = cast(
Any,
{
Expand Down Expand Up @@ -65,9 +73,19 @@
# NOTE: version causes conflict with global jsonschema validator
# See https://github.com/python-openapi/openapi-schema-validator/pull/12
# version="oas30",
id_of=lambda schema: (
schema.get("id", "") if isinstance(schema, dict) else ""
),
id_of=_oas30_id_of,
)

OAS30StrictValidator = extend(
OAS30Validator,
validators={
"type": oas_keywords.strict_type,
},
type_checker=oas_types.oas30_type_checker,
format_checker=oas_format.oas30_strict_format_checker,
# NOTE: version causes conflict with global jsonschema validator
# See https://github.com/python-openapi/openapi-schema-validator/pull/12
# version="oas30-strict",
)

OAS30ReadValidator = extend(
Expand All @@ -77,6 +95,7 @@
"writeOnly": oas_keywords.read_writeOnly,
},
)

OAS30WriteValidator = extend(
OAS30Validator,
validators={
Expand Down
Loading
Loading