Skip to content

Commit 2909023

Browse files
committed
Add OpenAPI 3.2.0 support (OAS32Validator)
OAS 3.2 still uses JSON Schema Draft 2020-12 (same as 3.1), so the new validator follows the same pattern as OAS31Validator. Adds OAS32Validator, oas32_format_checker, and oas32_type_checker with corresponding tests. Closes #255
1 parent 625bebd commit 2909023

File tree

5 files changed

+233
-0
lines changed

5 files changed

+233
-0
lines changed

openapi_schema_validator/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from openapi_schema_validator._format import oas30_format_checker
22
from openapi_schema_validator._format import oas30_strict_format_checker
33
from openapi_schema_validator._format import oas31_format_checker
4+
from openapi_schema_validator._format import oas32_format_checker
45
from openapi_schema_validator.shortcuts import validate
56
from openapi_schema_validator.validators import OAS30ReadValidator
67
from openapi_schema_validator.validators import OAS30StrictValidator
78
from openapi_schema_validator.validators import OAS30Validator
89
from openapi_schema_validator.validators import OAS30WriteValidator
910
from openapi_schema_validator.validators import OAS31Validator
11+
from openapi_schema_validator.validators import OAS32Validator
1012

1113
__author__ = "Artur Maciag"
1214
__email__ = "maciag.artur@gmail.com"
@@ -24,4 +26,6 @@
2426
"oas30_strict_format_checker",
2527
"OAS31Validator",
2628
"oas31_format_checker",
29+
"OAS32Validator",
30+
"oas32_format_checker",
2731
]

openapi_schema_validator/_format.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,10 @@ def is_password(instance: object) -> bool:
105105
oas31_format_checker.checks("float")(is_float)
106106
oas31_format_checker.checks("double")(is_double)
107107
oas31_format_checker.checks("password")(is_password)
108+
109+
oas32_format_checker = FormatChecker()
110+
oas32_format_checker.checks("int32")(is_int32)
111+
oas32_format_checker.checks("int64")(is_int64)
112+
oas32_format_checker.checks("float")(is_float)
113+
oas32_format_checker.checks("double")(is_double)
114+
oas32_format_checker.checks("password")(is_password)

openapi_schema_validator/_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ def is_string(checker: Any, instance: Any) -> bool:
3030
)
3131

3232
oas31_type_checker = draft202012_type_checker
33+
oas32_type_checker = draft202012_type_checker

openapi_schema_validator/validators.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from openapi_schema_validator import _keywords as oas_keywords
1414
from openapi_schema_validator import _types as oas_types
1515
from openapi_schema_validator._types import oas31_type_checker
16+
from openapi_schema_validator._types import oas32_type_checker
1617

1718

1819
def _oas30_id_of(schema: Any) -> str:
@@ -121,3 +122,21 @@ def _oas30_id_of(schema: Any) -> str:
121122
type_checker=oas31_type_checker,
122123
format_checker=oas_format.oas31_format_checker,
123124
)
125+
126+
OAS32Validator = extend(
127+
Draft202012Validator,
128+
{
129+
# adjusted to OAS
130+
"allOf": oas_keywords.allOf,
131+
"oneOf": oas_keywords.oneOf,
132+
"anyOf": oas_keywords.anyOf,
133+
"description": oas_keywords.not_implemented,
134+
# fixed OAS fields
135+
"discriminator": oas_keywords.not_implemented,
136+
"xml": oas_keywords.not_implemented,
137+
"externalDocs": oas_keywords.not_implemented,
138+
"example": oas_keywords.not_implemented,
139+
},
140+
type_checker=oas32_type_checker,
141+
format_checker=oas_format.oas32_format_checker,
142+
)

tests/integration/test_validators.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
from openapi_schema_validator import OAS30Validator
2020
from openapi_schema_validator import OAS30WriteValidator
2121
from openapi_schema_validator import OAS31Validator
22+
from openapi_schema_validator import OAS32Validator
2223
from openapi_schema_validator import oas30_format_checker
2324
from openapi_schema_validator import oas30_strict_format_checker
2425
from openapi_schema_validator import oas31_format_checker
26+
from openapi_schema_validator import oas32_format_checker
2527

2628

2729
class TestOAS30ValidatorFormatChecker:
@@ -1003,6 +1005,206 @@ def test_array_prefixitems_invalid(self, validator_class, value):
10031005
assert any(error in str(excinfo.value) for error in errors)
10041006

10051007

1008+
class TestOAS32ValidatorFormatChecker:
1009+
@pytest.fixture
1010+
def format_checker(self):
1011+
return OAS32Validator.FORMAT_CHECKER
1012+
1013+
def test_required_checkers(self, format_checker):
1014+
required_formats_set = {
1015+
# standard formats
1016+
"int32",
1017+
"int64",
1018+
"float",
1019+
"double",
1020+
"password",
1021+
}
1022+
assert required_formats_set.issubset(
1023+
set(format_checker.checkers.keys())
1024+
)
1025+
1026+
1027+
class TestOAS32ValidatorValidate(BaseTestOASValidatorValidate):
1028+
@pytest.fixture
1029+
def validator_class(self):
1030+
return OAS32Validator
1031+
1032+
@pytest.fixture
1033+
def format_checker(self):
1034+
return oas32_format_checker
1035+
1036+
@pytest.mark.parametrize("value", [b"test"])
1037+
def test_string_disallow_binary(self, validator_class, value):
1038+
schema = {"type": "string"}
1039+
validator = validator_class(schema)
1040+
1041+
with pytest.raises(ValidationError):
1042+
validator.validate(value)
1043+
1044+
@pytest.mark.parametrize(
1045+
"schema_type",
1046+
[
1047+
"boolean",
1048+
"array",
1049+
"integer",
1050+
"number",
1051+
"string",
1052+
],
1053+
)
1054+
def test_null(self, validator_class, schema_type):
1055+
schema = {"type": schema_type}
1056+
validator = validator_class(schema)
1057+
value = None
1058+
1059+
with pytest.raises(ValidationError):
1060+
validator.validate(value)
1061+
1062+
@pytest.mark.parametrize(
1063+
"schema_type",
1064+
[
1065+
"boolean",
1066+
"array",
1067+
"integer",
1068+
"number",
1069+
"string",
1070+
],
1071+
)
1072+
def test_nullable(self, validator_class, schema_type):
1073+
schema = {"type": [schema_type, "null"]}
1074+
validator = validator_class(schema)
1075+
value = None
1076+
1077+
result = validator.validate(value)
1078+
1079+
assert result is None
1080+
1081+
def test_schema_validation(self, validator_class, format_checker):
1082+
schema = {
1083+
"type": "object",
1084+
"required": ["name"],
1085+
"properties": {
1086+
"name": {"type": "string"},
1087+
"age": {
1088+
"type": "integer",
1089+
"format": "int32",
1090+
"minimum": 0,
1091+
},
1092+
"birth-date": {
1093+
"type": "string",
1094+
"format": "date",
1095+
},
1096+
},
1097+
"additionalProperties": False,
1098+
}
1099+
validator = validator_class(
1100+
schema,
1101+
format_checker=format_checker,
1102+
)
1103+
1104+
result = validator.validate({"name": "John", "age": 23})
1105+
assert result is None
1106+
1107+
with pytest.raises(ValidationError) as excinfo:
1108+
validator.validate({"name": "John", "city": "London"})
1109+
1110+
error = "Additional properties are not allowed ('city' was unexpected)"
1111+
assert error in str(excinfo.value)
1112+
1113+
with pytest.raises(ValidationError) as excinfo:
1114+
validator.validate({"name": "John", "birth-date": "-12"})
1115+
1116+
error = "'-12' is not a 'date'"
1117+
assert error in str(excinfo.value)
1118+
1119+
def test_schema_ref(self, validator_class, format_checker):
1120+
schema = {
1121+
"$ref": "#/$defs/Pet",
1122+
"$defs": {
1123+
"Pet": {
1124+
"required": ["id", "name"],
1125+
"properties": {
1126+
"id": {"type": "integer", "format": "int64"},
1127+
"name": {"type": "string"},
1128+
"tag": {"type": "string"},
1129+
},
1130+
}
1131+
},
1132+
}
1133+
validator = validator_class(
1134+
schema,
1135+
format_checker=format_checker,
1136+
)
1137+
1138+
result = validator.validate({"id": 1, "name": "John"})
1139+
assert result is None
1140+
1141+
with pytest.raises(ValidationError) as excinfo:
1142+
validator.validate({"name": "John"})
1143+
1144+
error = "'id' is a required property"
1145+
assert error in str(excinfo.value)
1146+
1147+
@pytest.mark.parametrize(
1148+
"value",
1149+
[
1150+
[1600, "Pennsylvania", "Avenue", "NW"],
1151+
[1600, "Pennsylvania", "Avenue"],
1152+
],
1153+
)
1154+
def test_array_prefixitems(self, validator_class, format_checker, value):
1155+
schema = {
1156+
"type": "array",
1157+
"prefixItems": [
1158+
{"type": "number"},
1159+
{"type": "string"},
1160+
{"enum": ["Street", "Avenue", "Boulevard"]},
1161+
{"enum": ["NW", "NE", "SW", "SE"]},
1162+
],
1163+
"items": False,
1164+
}
1165+
validator = validator_class(
1166+
schema,
1167+
format_checker=format_checker,
1168+
)
1169+
1170+
result = validator.validate(value)
1171+
1172+
assert result is None
1173+
1174+
@pytest.mark.parametrize(
1175+
"value",
1176+
[
1177+
[1600, "Pennsylvania", "Avenue", "NW", "Washington"],
1178+
],
1179+
)
1180+
def test_array_prefixitems_invalid(self, validator_class, value):
1181+
schema = {
1182+
"type": "array",
1183+
"prefixItems": [
1184+
{"type": "number"},
1185+
{"type": "string"},
1186+
{"enum": ["Street", "Avenue", "Boulevard"]},
1187+
{"enum": ["NW", "NE", "SW", "SE"]},
1188+
],
1189+
"items": False,
1190+
}
1191+
validator = validator_class(
1192+
schema,
1193+
format_checker=oas32_format_checker,
1194+
)
1195+
1196+
with pytest.raises(ValidationError) as excinfo:
1197+
validator.validate(value)
1198+
1199+
errors = [
1200+
# jsonschema < 4.20.0
1201+
"Expected at most 4 items, but found 5",
1202+
# jsonschema >= 4.20.0
1203+
"Expected at most 4 items but found 1 extra",
1204+
]
1205+
assert any(error in str(excinfo.value) for error in errors)
1206+
1207+
10061208
class TestOAS30StrictValidator:
10071209
"""
10081210
Tests for OAS30StrictValidator which follows OAS spec strictly:

0 commit comments

Comments
 (0)