Skip to content

Commit 7ae386b

Browse files
authored
Merge pull request #251 from python-openapi/feature/OAS30-strict-validator
OAS30 strict validator
2 parents 0b4a1f2 + af9beaa commit 7ae386b

File tree

8 files changed

+252
-13
lines changed

8 files changed

+252
-13
lines changed

README.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,57 @@ To validate an OpenAPI v3.1 schema:
9999
100100
By default, the latest OpenAPI schema syntax is expected.
101101

102+
103+
Strict vs Pragmatic Validators
104+
=============================
105+
106+
OpenAPI 3.0 has two validator variants with different behaviors for binary format:
107+
108+
**OAS30Validator (default - pragmatic)**
109+
- Accepts Python ``bytes`` for ``type: string`` with ``format: binary``
110+
- More lenient for Python use cases where binary data is common
111+
- Use when validating Python objects directly
112+
113+
**OAS30StrictValidator**
114+
- Follows OAS spec strictly: only accepts ``str`` for ``type: string``
115+
- For ``format: binary``, only accepts base64-encoded strings
116+
- Use when strict spec compliance is required
117+
118+
Comparison Matrix
119+
----------------
120+
121+
.. list-table::
122+
:header-rows: 1
123+
:widths: 35 20 22 23
124+
125+
* - Schema
126+
- Value
127+
- OAS30Validator (default)
128+
- OAS30StrictValidator
129+
* - ``type: string``
130+
- ``"test"`` (str)
131+
- Pass
132+
- Pass
133+
* - ``type: string``
134+
- ``b"test"`` (bytes)
135+
- **Fail**
136+
- **Fail**
137+
* - ``type: string, format: binary``
138+
- ``b"test"`` (bytes)
139+
- Pass
140+
- **Fail**
141+
* - ``type: string, format: binary``
142+
- ``"dGVzdA=="`` (base64)
143+
- Pass
144+
- Pass
145+
* - ``type: string, format: binary``
146+
- ``"test"`` (plain str)
147+
- Pass
148+
- **Fail**
149+
102150
For more details read about `Validation <https://openapi-schema-validator.readthedocs.io/en/latest/validation.html>`__.
103151

152+
104153
Related projects
105154
################
106155
* `openapi-core <https://github.com/python-openapi/openapi-core>`__

docs/validation.rst

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,66 @@ OpenAPI 3.0 schema comes with ``readOnly`` and ``writeOnly`` keywords. In order
137137
Traceback (most recent call last):
138138
...
139139
ValidationError: Tried to write read-only property with 23
140+
141+
Strict vs Pragmatic Validators
142+
------------------------------
143+
144+
OpenAPI 3.0 has two validator variants with different behaviors for binary format:
145+
146+
**OAS30Validator (default - pragmatic)**
147+
148+
- Accepts Python ``bytes`` for ``type: string`` with ``format: binary``
149+
- More lenient for Python use cases where binary data is common
150+
- Use when validating Python objects directly
151+
152+
**OAS30StrictValidator**
153+
154+
- Follows OAS spec strictly: only accepts ``str`` for ``type: string``
155+
- For ``format: binary``, only accepts base64-encoded strings
156+
- Use when strict spec compliance is required
157+
158+
Comparison Matrix
159+
~~~~~~~~~~~~~~~~~
160+
161+
.. list-table::
162+
:header-rows: 1
163+
:widths: 35 20 22 23
164+
165+
* - Schema
166+
- Value
167+
- OAS30Validator (default)
168+
- OAS30StrictValidator
169+
* - ``type: string``
170+
- ``"test"`` (str)
171+
- Pass
172+
- Pass
173+
* - ``type: string``
174+
- ``b"test"`` (bytes)
175+
- **Fail**
176+
- **Fail**
177+
* - ``type: string, format: binary``
178+
- ``b"test"`` (bytes)
179+
- Pass
180+
- **Fail**
181+
* - ``type: string, format: binary``
182+
- ``"dGVzdA=="`` (base64)
183+
- Pass
184+
- Pass
185+
* - ``type: string, format: binary``
186+
- ``"test"`` (plain str)
187+
- Pass
188+
- **Fail**
189+
190+
Example usage:
191+
192+
.. code-block:: python
193+
194+
from openapi_schema_validator import OAS30Validator, OAS30StrictValidator
195+
196+
# Pragmatic (default) - accepts bytes for binary format
197+
validator = OAS30Validator({"type": "string", "format": "binary"})
198+
validator.validate(b"binary data") # passes
199+
200+
# Strict - follows spec precisely
201+
validator = OAS30StrictValidator({"type": "string", "format": "binary"})
202+
validator.validate(b"binary data") # raises ValidationError

openapi_schema_validator/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from openapi_schema_validator._format import oas30_format_checker
2+
from openapi_schema_validator._format import oas30_strict_format_checker
23
from openapi_schema_validator._format import oas31_format_checker
34
from openapi_schema_validator.shortcuts import validate
45
from openapi_schema_validator.validators import OAS30ReadValidator
6+
from openapi_schema_validator.validators import OAS30StrictValidator
57
from openapi_schema_validator.validators import OAS30Validator
68
from openapi_schema_validator.validators import OAS30WriteValidator
79
from openapi_schema_validator.validators import OAS31Validator
@@ -15,9 +17,11 @@
1517
__all__ = [
1618
"validate",
1719
"OAS30ReadValidator",
20+
"OAS30StrictValidator",
1821
"OAS30WriteValidator",
1922
"OAS30Validator",
2023
"oas30_format_checker",
24+
"oas30_strict_format_checker",
2125
"OAS31Validator",
2226
"oas31_format_checker",
2327
]

openapi_schema_validator/_format.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,23 @@ def is_double(instance: object) -> bool:
4444
return isinstance(instance, float)
4545

4646

47-
def is_binary(instance: object) -> bool:
48-
if not isinstance(instance, (str, bytes)):
49-
return True
50-
if isinstance(instance, str):
47+
def is_binary_strict(instance: object) -> bool:
48+
# Strict: only accepts base64-encoded strings, not raw bytes
49+
if isinstance(instance, bytes):
5150
return False
51+
if isinstance(instance, str):
52+
try:
53+
b64decode(instance)
54+
return True
55+
except Exception:
56+
return False
57+
return True
58+
59+
60+
def is_binary_pragmatic(instance: object) -> bool:
61+
# Pragmatic: accepts bytes (common in Python) or base64-encoded strings
62+
if isinstance(instance, (str, bytes)):
63+
return True
5264
return True
5365

5466

@@ -72,10 +84,21 @@ def is_password(instance: object) -> bool:
7284
oas30_format_checker.checks("int64")(is_int64)
7385
oas30_format_checker.checks("float")(is_float)
7486
oas30_format_checker.checks("double")(is_double)
75-
oas30_format_checker.checks("binary")(is_binary)
87+
oas30_format_checker.checks("binary")(is_binary_pragmatic)
7688
oas30_format_checker.checks("byte", (binascii.Error, TypeError))(is_byte)
7789
oas30_format_checker.checks("password")(is_password)
7890

91+
oas30_strict_format_checker = FormatChecker()
92+
oas30_strict_format_checker.checks("int32")(is_int32)
93+
oas30_strict_format_checker.checks("int64")(is_int64)
94+
oas30_strict_format_checker.checks("float")(is_float)
95+
oas30_strict_format_checker.checks("double")(is_double)
96+
oas30_strict_format_checker.checks("binary")(is_binary_strict)
97+
oas30_strict_format_checker.checks("byte", (binascii.Error, TypeError))(
98+
is_byte
99+
)
100+
oas30_strict_format_checker.checks("password")(is_password)
101+
79102
oas31_format_checker = FormatChecker()
80103
oas31_format_checker.checks("int32")(is_int32)
81104
oas31_format_checker.checks("int64")(is_int64)

openapi_schema_validator/_keywords.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def type(
115115
instance: Any,
116116
schema: Mapping[str, Any],
117117
) -> Iterator[ValidationError]:
118+
"""Default type validator - allows Python bytes for binary format for pragmatic reasons."""
118119
if instance is None:
119120
# nullable implementation based on OAS 3.0.3
120121
# * nullable is only meaningful if its value is true
@@ -125,6 +126,34 @@ def type(
125126
return
126127
yield ValidationError("None for not nullable")
127128

129+
# Pragmatic: allow bytes for binary format (common in Python use cases)
130+
if (
131+
data_type == "string"
132+
and schema.get("format") == "binary"
133+
and isinstance(instance, bytes)
134+
):
135+
return
136+
137+
if not validator.is_type(instance, data_type):
138+
data_repr = repr(data_type)
139+
yield ValidationError(f"{instance!r} is not of type {data_repr}")
140+
141+
142+
def strict_type(
143+
validator: Any,
144+
data_type: str,
145+
instance: Any,
146+
schema: Any,
147+
) -> Any:
148+
"""
149+
Strict type validator - follows OAS spec precisely.
150+
Does NOT allow Python bytes for binary format.
151+
"""
152+
if instance is None:
153+
if schema.get("nullable") is True:
154+
return
155+
yield ValidationError("None for not nullable")
156+
128157
if not validator.is_type(instance, data_type):
129158
data_repr = repr(data_type)
130159
yield ValidationError(f"{instance!r} is not of type {data_repr}")

openapi_schema_validator/_types.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212

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

1617

1718
oas30_type_checker = TypeChecker(
@@ -27,4 +28,5 @@ def is_string(checker: Any, instance: Any) -> bool:
2728
},
2829
),
2930
)
31+
3032
oas31_type_checker = draft202012_type_checker

openapi_schema_validator/validators.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from jsonschema import _keywords
55
from jsonschema import _legacy_keywords
6+
from jsonschema.exceptions import ValidationError
67
from jsonschema.validators import Draft202012Validator
78
from jsonschema.validators import create
89
from jsonschema.validators import extend
@@ -13,6 +14,13 @@
1314
from openapi_schema_validator import _types as oas_types
1415
from openapi_schema_validator._types import oas31_type_checker
1516

17+
18+
def _oas30_id_of(schema: Any) -> str:
19+
if isinstance(schema, dict):
20+
return schema.get("id", "") # type: ignore[no-any-return]
21+
return ""
22+
23+
1624
OAS30_VALIDATORS = cast(
1725
Any,
1826
{
@@ -65,9 +73,19 @@
6573
# NOTE: version causes conflict with global jsonschema validator
6674
# See https://github.com/python-openapi/openapi-schema-validator/pull/12
6775
# version="oas30",
68-
id_of=lambda schema: (
69-
schema.get("id", "") if isinstance(schema, dict) else ""
70-
),
76+
id_of=_oas30_id_of,
77+
)
78+
79+
OAS30StrictValidator = extend(
80+
OAS30Validator,
81+
validators={
82+
"type": oas_keywords.strict_type,
83+
},
84+
type_checker=oas_types.oas30_type_checker,
85+
format_checker=oas_format.oas30_strict_format_checker,
86+
# NOTE: version causes conflict with global jsonschema validator
87+
# See https://github.com/python-openapi/openapi-schema-validator/pull/12
88+
# version="oas30-strict",
7189
)
7290

7391
OAS30ReadValidator = extend(
@@ -77,6 +95,7 @@
7795
"writeOnly": oas_keywords.read_writeOnly,
7896
},
7997
)
98+
8099
OAS30WriteValidator = extend(
81100
OAS30Validator,
82101
validators={

0 commit comments

Comments
 (0)