Skip to content

Commit d2a7eab

Browse files
authored
Merge pull request #18 from anexia-it/master
python-openapi/openapi-core#296: Implements OpenAPI 3.1 validator
2 parents 22c5892 + bea387d commit d2a7eab

File tree

8 files changed

+275
-14
lines changed

8 files changed

+275
-14
lines changed

.github/workflows/python-test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: [3.6, 3.7, 3.8, 3.9]
17+
python-version: [3.7, 3.8, 3.9]
1818
fail-fast: false
1919
steps:
2020
- uses: actions/checkout@v2

README.rst

+7-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ openapi-schema-validator
1818
About
1919
#####
2020

21-
Openapi-schema-validator is a Python library that validates schema against the `OpenAPI Schema Specification v3.0 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject>`__ which is an extended subset of the `JSON Schema Specification Wright Draft 00 <http://json-schema.org/>`__.
21+
Openapi-schema-validator is a Python library that validates schema against:
22+
23+
* `OpenAPI Schema Specification v3.0 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject>`__ which is an extended subset of the `JSON Schema Specification Wright Draft 00 <http://json-schema.org/>`__.
24+
* `OpenAPI Schema Specification v3.1 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#schemaObject>`__ which is an extended superset of the `JSON Schema Specification Draft 2020-12 <http://json-schema.org/>`__.
2225

2326
Installation
2427
############
@@ -47,7 +50,7 @@ Simple usage
4750
4851
# A sample schema
4952
schema = {
50-
"type" : "object",
53+
"type": "object",
5154
"required": [
5255
"name"
5356
],
@@ -82,9 +85,9 @@ You can also check format for primitive types
8285

8386
.. code-block:: python
8487
85-
from openapi_schema_validator import oas30_format_checker
88+
from openapi_schema_validator import oas31_format_checker
8689
87-
validate({"name": "John", "birth-date": "-12"}, schema, format_checker=oas30_format_checker)
90+
validate({"name": "John", "birth-date": "-12"}, schema, format_checker=oas31_format_checker)
8891
8992
Traceback (most recent call last):
9093
...

openapi_schema_validator/__init__.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
# -*- coding: utf-8 -*-
2-
from openapi_schema_validator._format import oas30_format_checker
2+
from openapi_schema_validator._format import oas30_format_checker, \
3+
oas31_format_checker
34
from openapi_schema_validator.shortcuts import validate
4-
from openapi_schema_validator.validators import OAS30Validator
5+
from openapi_schema_validator.validators import OAS30Validator, OAS31Validator
56

67
__author__ = 'Artur Maciag'
78
__email__ = '[email protected]'
89
__version__ = '0.2.0'
910
__url__ = 'https://github.com/p1c2u/openapi-schema-validator'
1011
__license__ = '3-clause BSD License'
1112

12-
__all__ = ['validate', 'OAS30Validator', 'oas30_format_checker']
13+
__all__ = [
14+
'validate',
15+
'OAS30Validator',
16+
'oas30_format_checker',
17+
'OAS31Validator',
18+
'oas31_format_checker',
19+
]

openapi_schema_validator/_format.py

+1
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,4 @@ def check(self, instance, format):
144144

145145

146146
oas30_format_checker = OASFormatChecker()
147+
oas31_format_checker = oas30_format_checker

openapi_schema_validator/_types.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from jsonschema._types import (
22
TypeChecker, is_array, is_bool, is_integer,
3-
is_object, is_number,
3+
is_object, is_number, draft202012_type_checker,
44
)
55

66

@@ -18,3 +18,4 @@ def is_string(checker, instance):
1818
u"object": is_object,
1919
},
2020
)
21+
oas31_type_checker = draft202012_type_checker

openapi_schema_validator/shortcuts.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from jsonschema.exceptions import best_match
22

3-
from openapi_schema_validator.validators import OAS30Validator
3+
from openapi_schema_validator.validators import OAS31Validator
44

55

6-
def validate(instance, schema, cls=OAS30Validator, *args, **kwargs):
6+
def validate(instance, schema, cls=OAS31Validator, *args, **kwargs):
77
cls.check_schema(schema)
88
validator = cls(schema, *args, **kwargs)
99
error = best_match(validator.iter_errors(instance))

openapi_schema_validator/validators.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
from copy import deepcopy
33

44
from jsonschema import _legacy_validators, _utils, _validators
5-
from jsonschema.validators import create
5+
from jsonschema.validators import create, Draft202012Validator, extend
66

77
from openapi_schema_validator import _types as oas_types
88
from openapi_schema_validator import _validators as oas_validators
9-
9+
from openapi_schema_validator._types import oas31_type_checker
1010

1111
BaseOAS30Validator = create(
1212
meta_schema=_utils.load_schema("draft4"),
@@ -56,6 +56,21 @@
5656
id_of=lambda schema: schema.get(u"id", ""),
5757
)
5858

59+
BaseOAS31Validator = extend(
60+
Draft202012Validator,
61+
{
62+
# adjusted to OAS
63+
u"description": oas_validators.not_implemented,
64+
u"format": oas_validators.format,
65+
# fixed OAS fields
66+
u"discriminator": oas_validators.not_implemented,
67+
u"xml": oas_validators.not_implemented,
68+
u"externalDocs": oas_validators.not_implemented,
69+
u"example": oas_validators.not_implemented,
70+
},
71+
type_checker=oas31_type_checker,
72+
)
73+
5974

6075
@attrs
6176
class OAS30Validator(BaseOAS30Validator):
@@ -76,3 +91,7 @@ def iter_errors(self, instance, _schema=None):
7691

7792
validator = self.evolve(schema=_schema)
7893
return super(OAS30Validator, validator).iter_errors(instance)
94+
95+
96+
class OAS31Validator(BaseOAS31Validator):
97+
pass

tests/integration/test_validators.py

+231-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from jsonschema import ValidationError
22
import pytest
33

4-
from openapi_schema_validator import OAS30Validator, oas30_format_checker
4+
from openapi_schema_validator import OAS30Validator, oas30_format_checker, \
5+
OAS31Validator, oas31_format_checker
56

67
try:
78
from unittest import mock
@@ -237,3 +238,232 @@ def test_oneof_required(self):
237238
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
238239
result = validator.validate(instance)
239240
assert result is None
241+
242+
243+
class TestOAS31ValidatorValidate(object):
244+
@pytest.mark.parametrize('schema_type', [
245+
'boolean', 'array', 'integer', 'number', 'string',
246+
])
247+
def test_null(self, schema_type):
248+
schema = {"type": schema_type}
249+
validator = OAS31Validator(schema)
250+
value = None
251+
252+
with pytest.raises(ValidationError):
253+
validator.validate(value)
254+
255+
@pytest.mark.parametrize('schema_type', [
256+
'boolean', 'array', 'integer', 'number', 'string',
257+
])
258+
def test_nullable(self, schema_type):
259+
schema = {"type": [schema_type, 'null']}
260+
validator = OAS31Validator(schema)
261+
value = None
262+
263+
result = validator.validate(value)
264+
265+
assert result is None
266+
267+
@pytest.mark.parametrize('value', [
268+
'1989-01-02T00:00:00Z',
269+
'2018-01-02T23:59:59Z',
270+
])
271+
@mock.patch(
272+
'openapi_schema_validator._format.'
273+
'DATETIME_HAS_RFC3339_VALIDATOR', False
274+
)
275+
@mock.patch(
276+
'openapi_schema_validator._format.'
277+
'DATETIME_HAS_STRICT_RFC3339', False
278+
)
279+
@mock.patch(
280+
'openapi_schema_validator._format.'
281+
'DATETIME_HAS_ISODATE', False
282+
)
283+
def test_string_format_no_datetime_validator(self, value):
284+
schema = {"type": 'string', "format": 'date-time'}
285+
validator = OAS31Validator(
286+
schema,
287+
format_checker=oas31_format_checker,
288+
)
289+
290+
result = validator.validate(value)
291+
292+
assert result is None
293+
294+
@pytest.mark.parametrize('value', [
295+
'1989-01-02T00:00:00Z',
296+
'2018-01-02T23:59:59Z',
297+
])
298+
@mock.patch(
299+
'openapi_schema_validator._format.'
300+
'DATETIME_HAS_RFC3339_VALIDATOR', True
301+
)
302+
@mock.patch(
303+
'openapi_schema_validator._format.'
304+
'DATETIME_HAS_STRICT_RFC3339', False
305+
)
306+
@mock.patch(
307+
'openapi_schema_validator._format.'
308+
'DATETIME_HAS_ISODATE', False
309+
)
310+
def test_string_format_datetime_rfc3339_validator(self, value):
311+
schema = {"type": 'string', "format": 'date-time'}
312+
validator = OAS31Validator(
313+
schema,
314+
format_checker=oas31_format_checker,
315+
)
316+
317+
result = validator.validate(value)
318+
319+
assert result is None
320+
321+
@pytest.mark.parametrize('value', [
322+
'1989-01-02T00:00:00Z',
323+
'2018-01-02T23:59:59Z',
324+
])
325+
@mock.patch(
326+
'openapi_schema_validator._format.'
327+
'DATETIME_HAS_RFC3339_VALIDATOR', False
328+
)
329+
@mock.patch(
330+
'openapi_schema_validator._format.'
331+
'DATETIME_HAS_STRICT_RFC3339', True
332+
)
333+
@mock.patch(
334+
'openapi_schema_validator._format.'
335+
'DATETIME_HAS_ISODATE', False
336+
)
337+
def test_string_format_datetime_strict_rfc3339(self, value):
338+
schema = {"type": 'string', "format": 'date-time'}
339+
validator = OAS31Validator(
340+
schema,
341+
format_checker=oas31_format_checker,
342+
)
343+
344+
result = validator.validate(value)
345+
346+
assert result is None
347+
348+
@pytest.mark.parametrize('value', [
349+
'1989-01-02T00:00:00Z',
350+
'2018-01-02T23:59:59Z',
351+
])
352+
@mock.patch(
353+
'openapi_schema_validator._format.'
354+
'DATETIME_HAS_RFC3339_VALIDATOR', False
355+
)
356+
@mock.patch(
357+
'openapi_schema_validator._format.'
358+
'DATETIME_HAS_STRICT_RFC3339', False
359+
)
360+
@mock.patch(
361+
'openapi_schema_validator._format.'
362+
'DATETIME_HAS_ISODATE', True
363+
)
364+
def test_string_format_datetime_isodate(self, value):
365+
schema = {"type": 'string', "format": 'date-time'}
366+
validator = OAS31Validator(
367+
schema,
368+
format_checker=oas31_format_checker,
369+
)
370+
371+
result = validator.validate(value)
372+
373+
assert result is None
374+
375+
@pytest.mark.parametrize('value', [
376+
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0',
377+
'F50EC0B7-F960-400D-91F0-C42A6D44E3D0',
378+
])
379+
def test_string_uuid(self, value):
380+
schema = {"type": 'string', "format": 'uuid'}
381+
validator = OAS31Validator(
382+
schema,
383+
format_checker=oas31_format_checker,
384+
)
385+
386+
result = validator.validate(value)
387+
388+
assert result is None
389+
390+
def test_schema_validation(self):
391+
schema = {
392+
"type": "object",
393+
"required": [
394+
"name"
395+
],
396+
"properties": {
397+
"name": {
398+
"type": "string"
399+
},
400+
"age": {
401+
"type": "integer",
402+
"format": "int32",
403+
"minimum": 0,
404+
"nullable": True,
405+
},
406+
"birth-date": {
407+
"type": "string",
408+
"format": "date",
409+
}
410+
},
411+
"additionalProperties": False,
412+
}
413+
validator = OAS31Validator(
414+
schema,
415+
format_checker=oas31_format_checker,
416+
)
417+
418+
result = validator.validate({"name": "John", "age": 23}, schema)
419+
assert result is None
420+
421+
with pytest.raises(ValidationError) as excinfo:
422+
validator.validate({"name": "John", "city": "London"}, schema)
423+
424+
error = "Additional properties are not allowed ('city' was unexpected)"
425+
assert error in str(excinfo.value)
426+
427+
with pytest.raises(ValidationError) as excinfo:
428+
validator.validate({"name": "John", "birth-date": "-12"})
429+
430+
error = "'-12' is not a 'date'"
431+
assert error in str(excinfo.value)
432+
433+
def test_schema_ref(self):
434+
schema = {
435+
"$ref": "#/$defs/Pet",
436+
"$defs": {
437+
"Pet": {
438+
"required": [
439+
"id",
440+
"name"
441+
],
442+
"properties": {
443+
"id": {
444+
"type": "integer",
445+
"format": "int64"
446+
},
447+
"name": {
448+
"type": "string"
449+
},
450+
"tag": {
451+
"type": "string"
452+
}
453+
}
454+
}
455+
}
456+
}
457+
validator = OAS31Validator(
458+
schema,
459+
format_checker=oas31_format_checker,
460+
)
461+
462+
result = validator.validate({"id": 1, "name": "John"}, schema)
463+
assert result is None
464+
465+
with pytest.raises(ValidationError) as excinfo:
466+
validator.validate({"name": "John"}, schema)
467+
468+
error = "'id' is a required property"
469+
assert error in str(excinfo.value)

0 commit comments

Comments
 (0)