Skip to content

p1c2u/openapi-core#296: Implements OpenAPI 3.1 validator #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 11, 2022
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
2 changes: 1 addition & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: [3.7, 3.8, 3.9]
fail-fast: false
steps:
- uses: actions/checkout@v2
Expand Down
11 changes: 7 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ openapi-schema-validator
About
#####

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/>`__.
Openapi-schema-validator is a Python library that validates schema against:

* `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/>`__.
* `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/>`__.

Installation
############
Expand Down Expand Up @@ -47,7 +50,7 @@ Simple usage

# A sample schema
schema = {
"type" : "object",
"type": "object",
"required": [
"name"
],
Expand Down Expand Up @@ -82,9 +85,9 @@ You can also check format for primitive types

.. code-block:: python

from openapi_schema_validator import oas30_format_checker
from openapi_schema_validator import oas31_format_checker

validate({"name": "John", "birth-date": "-12"}, schema, format_checker=oas30_format_checker)
validate({"name": "John", "birth-date": "-12"}, schema, format_checker=oas31_format_checker)

Traceback (most recent call last):
...
Expand Down
13 changes: 10 additions & 3 deletions openapi_schema_validator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# -*- coding: utf-8 -*-
from openapi_schema_validator._format import oas30_format_checker
from openapi_schema_validator._format import oas30_format_checker, \
oas31_format_checker
from openapi_schema_validator.shortcuts import validate
from openapi_schema_validator.validators import OAS30Validator
from openapi_schema_validator.validators import OAS30Validator, OAS31Validator

__author__ = 'Artur Maciag'
__email__ = 'maciag.artur@gmail.com'
__version__ = '0.2.0'
__url__ = 'https://github.com/p1c2u/openapi-schema-validator'
__license__ = '3-clause BSD License'

__all__ = ['validate', 'OAS30Validator', 'oas30_format_checker']
__all__ = [
'validate',
'OAS30Validator',
'oas30_format_checker',
'OAS31Validator',
'oas31_format_checker',
]
1 change: 1 addition & 0 deletions openapi_schema_validator/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,4 @@ def check(self, instance, format):


oas30_format_checker = OASFormatChecker()
oas31_format_checker = oas30_format_checker
3 changes: 2 additions & 1 deletion openapi_schema_validator/_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from jsonschema._types import (
TypeChecker, is_array, is_bool, is_integer,
is_object, is_number,
is_object, is_number, draft202012_type_checker,
)


Expand All @@ -18,3 +18,4 @@ def is_string(checker, instance):
u"object": is_object,
},
)
oas31_type_checker = draft202012_type_checker
4 changes: 2 additions & 2 deletions openapi_schema_validator/shortcuts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from jsonschema.exceptions import best_match

from openapi_schema_validator.validators import OAS30Validator
from openapi_schema_validator.validators import OAS31Validator


def validate(instance, schema, cls=OAS30Validator, *args, **kwargs):
def validate(instance, schema, cls=OAS31Validator, *args, **kwargs):
cls.check_schema(schema)
validator = cls(schema, *args, **kwargs)
error = best_match(validator.iter_errors(instance))
Expand Down
23 changes: 21 additions & 2 deletions openapi_schema_validator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from copy import deepcopy

from jsonschema import _legacy_validators, _utils, _validators
from jsonschema.validators import create
from jsonschema.validators import create, Draft202012Validator, extend

from openapi_schema_validator import _types as oas_types
from openapi_schema_validator import _validators as oas_validators

from openapi_schema_validator._types import oas31_type_checker

BaseOAS30Validator = create(
meta_schema=_utils.load_schema("draft4"),
Expand Down Expand Up @@ -56,6 +56,21 @@
id_of=lambda schema: schema.get(u"id", ""),
)

BaseOAS31Validator = extend(
Draft202012Validator,
{
# adjusted to OAS
u"description": oas_validators.not_implemented,
u"format": oas_validators.format,
# fixed OAS fields
u"discriminator": oas_validators.not_implemented,
u"xml": oas_validators.not_implemented,
u"externalDocs": oas_validators.not_implemented,
u"example": oas_validators.not_implemented,
},
type_checker=oas31_type_checker,
)


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

validator = self.evolve(schema=_schema)
return super(OAS30Validator, validator).iter_errors(instance)


class OAS31Validator(BaseOAS31Validator):
pass
232 changes: 231 additions & 1 deletion tests/integration/test_validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from jsonschema import ValidationError
import pytest

from openapi_schema_validator import OAS30Validator, oas30_format_checker
from openapi_schema_validator import OAS30Validator, oas30_format_checker, \
OAS31Validator, oas31_format_checker

try:
from unittest import mock
Expand Down Expand Up @@ -237,3 +238,232 @@ def test_oneof_required(self):
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
result = validator.validate(instance)
assert result is None


class TestOAS31ValidatorValidate(object):
@pytest.mark.parametrize('schema_type', [
'boolean', 'array', 'integer', 'number', 'string',
])
def test_null(self, schema_type):
schema = {"type": schema_type}
validator = OAS31Validator(schema)
value = None

with pytest.raises(ValidationError):
validator.validate(value)

@pytest.mark.parametrize('schema_type', [
'boolean', 'array', 'integer', 'number', 'string',
])
def test_nullable(self, schema_type):
schema = {"type": [schema_type, 'null']}
validator = OAS31Validator(schema)
value = None

result = validator.validate(value)

assert result is None

@pytest.mark.parametrize('value', [
'1989-01-02T00:00:00Z',
'2018-01-02T23:59:59Z',
])
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_RFC3339_VALIDATOR', False
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_STRICT_RFC3339', False
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_ISODATE', False
)
def test_string_format_no_datetime_validator(self, value):
schema = {"type": 'string', "format": 'date-time'}
validator = OAS31Validator(
schema,
format_checker=oas31_format_checker,
)

result = validator.validate(value)

assert result is None

@pytest.mark.parametrize('value', [
'1989-01-02T00:00:00Z',
'2018-01-02T23:59:59Z',
])
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_RFC3339_VALIDATOR', True
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_STRICT_RFC3339', False
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_ISODATE', False
)
def test_string_format_datetime_rfc3339_validator(self, value):
schema = {"type": 'string', "format": 'date-time'}
validator = OAS31Validator(
schema,
format_checker=oas31_format_checker,
)

result = validator.validate(value)

assert result is None

@pytest.mark.parametrize('value', [
'1989-01-02T00:00:00Z',
'2018-01-02T23:59:59Z',
])
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_RFC3339_VALIDATOR', False
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_STRICT_RFC3339', True
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_ISODATE', False
)
def test_string_format_datetime_strict_rfc3339(self, value):
schema = {"type": 'string', "format": 'date-time'}
validator = OAS31Validator(
schema,
format_checker=oas31_format_checker,
)

result = validator.validate(value)

assert result is None

@pytest.mark.parametrize('value', [
'1989-01-02T00:00:00Z',
'2018-01-02T23:59:59Z',
])
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_RFC3339_VALIDATOR', False
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_STRICT_RFC3339', False
)
@mock.patch(
'openapi_schema_validator._format.'
'DATETIME_HAS_ISODATE', True
)
def test_string_format_datetime_isodate(self, value):
schema = {"type": 'string', "format": 'date-time'}
validator = OAS31Validator(
schema,
format_checker=oas31_format_checker,
)

result = validator.validate(value)

assert result is None

@pytest.mark.parametrize('value', [
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0',
'F50EC0B7-F960-400D-91F0-C42A6D44E3D0',
])
def test_string_uuid(self, value):
schema = {"type": 'string', "format": 'uuid'}
validator = OAS31Validator(
schema,
format_checker=oas31_format_checker,
)

result = validator.validate(value)

assert result is None

def test_schema_validation(self):
schema = {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer",
"format": "int32",
"minimum": 0,
"nullable": True,
},
"birth-date": {
"type": "string",
"format": "date",
}
},
"additionalProperties": False,
}
validator = OAS31Validator(
schema,
format_checker=oas31_format_checker,
)

result = validator.validate({"name": "John", "age": 23}, schema)
assert result is None

with pytest.raises(ValidationError) as excinfo:
validator.validate({"name": "John", "city": "London"}, schema)

error = "Additional properties are not allowed ('city' was unexpected)"
assert error in str(excinfo.value)

with pytest.raises(ValidationError) as excinfo:
validator.validate({"name": "John", "birth-date": "-12"})

error = "'-12' is not a 'date'"
assert error in str(excinfo.value)

def test_schema_ref(self):
schema = {
"$ref": "#/$defs/Pet",
"$defs": {
"Pet": {
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}
}
validator = OAS31Validator(
schema,
format_checker=oas31_format_checker,
)

result = validator.validate({"id": 1, "name": "John"}, schema)
assert result is None

with pytest.raises(ValidationError) as excinfo:
validator.validate({"name": "John"}, schema)

error = "'id' is a required property"
assert error in str(excinfo.value)