Skip to content

Commit

Permalink
Merge pull request #78 from atmo/draft7_support
Browse files Browse the repository at this point in the history
JSON schema draft 7 support
  • Loading branch information
fuhrysteve authored Jul 21, 2019
2 parents c65d473 + bf22eb7 commit 87a66c1
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 389 deletions.
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ matrix:
python: "3.5"
- os: linux
python: "2.7"
env: TOXENV=py27-marshmallow2
- os: linux
python: "2.7"
env: TOXENV=py27-marshmallow3
- os: linux
python: "pypy3"
- os: linux
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)

marshmallow-jsonschema translates marshmallow schemas into
JSON Schema Draft v4 compliant jsonschema. See http://json-schema.org/
JSON Schema Draft v7 compliant jsonschema. See http://json-schema.org/

#### Why would I want my schema translated to JSON?

Expand Down
3 changes: 2 additions & 1 deletion marshmallow_jsonschema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
__version__ = get_distribution("marshmallow-jsonschema").version
__license__ = "MIT"

from .base import JSONSchema, UnsupportedValueError
from .base import JSONSchema
from .exceptions import UnsupportedValueError

__all__ = ("JSONSchema", "UnsupportedValueError")
29 changes: 10 additions & 19 deletions marshmallow_jsonschema/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,14 @@
basestring,
dot_data_backwards_compatible,
list_inner,
INCLUDE,
EXCLUDE,
RAISE,
)
from .exceptions import UnsupportedValueError
from .validation import handle_length, handle_one_of, handle_range

try:
from marshmallow import RAISE, INCLUDE, EXCLUDE
except ImportError:
RAISE = "raise"
INCLUDE = "include"
EXCLUDE = "exclude"

__all__ = ("JSONSchema", "UnsupportedValueError")


class UnsupportedValueError(Exception):
pass

__all__ = ("JSONSchema",)

TYPE_MAP = {
dict: {"type": "object"},
Expand Down Expand Up @@ -207,9 +199,9 @@ def _from_nested_schema(self, obj, field):
wrapped_nested.dump(nested_instance)
)

additional_properties = _resolve_additional_properties(nested_cls)
if additional_properties is not None:
wrapped_dumped["additionalProperties"] = additional_properties
wrapped_dumped["additionalProperties"] = _resolve_additional_properties(
nested_cls
)

self._nested_schema_classes[name] = wrapped_dumped

Expand Down Expand Up @@ -249,12 +241,11 @@ def wrap(self, data, **_):
cls = self.obj.__class__
name = cls.__name__

additional_properties = _resolve_additional_properties(cls)
if additional_properties is not None:
data["additionalProperties"] = additional_properties
data["additionalProperties"] = _resolve_additional_properties(cls)

self._nested_schema_classes[name] = data
root = {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": self._nested_schema_classes,
"$ref": "#/definitions/{name}".format(name=name),
}
Expand Down
14 changes: 9 additions & 5 deletions marshmallow_jsonschema/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
import marshmallow

PY2 = int(sys.version_info[0]) == 2

MARSHMALLOW_MAJOR_VERSION = int(marshmallow.__version__.split(".", 1)[0])

MARSHMALLOW_2 = MARSHMALLOW_MAJOR_VERSION == 2
MARSHMALLOW_3 = MARSHMALLOW_MAJOR_VERSION == 3

if PY2:
text_type = unicode
binary_type = str
Expand All @@ -14,7 +18,10 @@
basestring = (str, bytes)


if MARSHMALLOW_MAJOR_VERSION == 2:
if MARSHMALLOW_2:
RAISE = "raise"
INCLUDE = "include"
EXCLUDE = "exclude"

def dot_data_backwards_compatible(json_schema):
return json_schema.data
Expand All @@ -24,15 +31,12 @@ def list_inner(list_field):


else:
from marshmallow import RAISE, INCLUDE, EXCLUDE

def dot_data_backwards_compatible(json_schema):
return json_schema

def list_inner(list_field):
if hasattr(list_field, "container"):
# backwards compatibility for marshmallow versions prior to 3.0.0rc8
return list_field.container

return list_field.inner


Expand Down
2 changes: 2 additions & 0 deletions marshmallow_jsonschema/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class UnsupportedValueError(Exception):
pass
50 changes: 34 additions & 16 deletions marshmallow_jsonschema/validation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from marshmallow import fields

from .exceptions import UnsupportedValueError


def handle_length(schema, field, validator, parent_schema):
"""Adds validation logic for ``marshmallow.validate.Length``, setting the
Expand All @@ -21,7 +23,7 @@ def handle_length(schema, field, validator, parent_schema):
altered.
Raises:
ValueError: Raised if the `field` is something other than
UnsupportedValueError: Raised if the `field` is something other than
`fields.List`, `fields.Nested`, or `fields.String`
"""
if isinstance(field, fields.String):
Expand All @@ -31,9 +33,9 @@ def handle_length(schema, field, validator, parent_schema):
minKey = "minItems"
maxKey = "maxItems"
else:
raise ValueError(
raise UnsupportedValueError(
"In order to set the Length validator for JSON "
"schema, the field must be either a List or a String"
"schema, the field must be either a List, Nested or a String"
)

if validator.min:
Expand Down Expand Up @@ -64,12 +66,11 @@ def handle_one_of(schema, field, validator, parent_schema):
belongs to.
Returns:
dict: A, possibly, new JSON Schema that has been post processed and
dict: New JSON Schema that has been post processed and
altered.
"""
if validator.choices:
schema["enum"] = list(validator.choices)
schema["enumNames"] = list(validator.labels)
schema["enum"] = list(validator.choices)
schema["enumNames"] = list(validator.labels)

return schema

Expand All @@ -83,22 +84,39 @@ def handle_range(schema, field, validator, parent_schema):
want to post-process.
field (fields.Field): The field that generated the original schema and
who this post-processor belongs to.
validator (marshmallow.validate.Length): The validator attached to the
validator (marshmallow.validate.Range): The validator attached to the
passed in field.
parent_schema (marshmallow.Schema): The Schema instance that the field
belongs to.
Returns:
dict: A, possibly, new JSON Schema that has been post processed and
dict: New JSON Schema that has been post processed and
altered.
Raises:
UnsupportedValueError: Raised if the `field` is not an instance of
`fields.Number`.
"""
if not isinstance(field, fields.Number):
return schema

if validator.min:
schema["minimum"] = validator.min

if validator.max:
schema["maximum"] = validator.max
raise UnsupportedValueError(
"'Range' validator for non-number fields is not supported"
)

if validator.min is not None:
# marshmallow 2 includes minimum by default
# marshmallow 3 supports "min_inclusive"
min_inclusive = getattr(validator, "min_inclusive", True)
if min_inclusive:
schema["minimum"] = validator.min
else:
schema["exclusiveMinimum"] = validator.min

if validator.max is not None:
# marshmallow 2 includes maximum by default
# marshmallow 3 supports "max_inclusive"
max_inclusive = getattr(validator, "max_inclusive", True)
if max_inclusive:
schema["maximum"] = validator.max
else:
schema["exclusiveMaximum"] = validator.max
return schema
2 changes: 1 addition & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
coverage>=4.5.3
jsonschema>=2.6.0
jsonschema>=3.0.1
pytest>=4.6.3
pytest-cov

Expand Down
21 changes: 21 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from jsonschema import Draft7Validator
from marshmallow import Schema, fields, validate

from marshmallow_jsonschema import JSONSchema
from marshmallow_jsonschema.compat import dot_data_backwards_compatible


class Address(Schema):
id = fields.String(default="no-id")
Expand Down Expand Up @@ -48,3 +52,20 @@ class UserSchema(Schema):
)
github = fields.Nested(GithubProfile)
const = fields.String(validate=validate.Length(equal=50))


def _validate_schema(schema):
"""
raises jsonschema.exceptions.SchemaError
"""
Draft7Validator.check_schema(schema)


def validate_and_dump(schema):
json_schema = JSONSchema()
dumped = json_schema.dump(schema)
data = dot_data_backwards_compatible(dumped)
_validate_schema(data)
# ensure last version
assert data["$schema"] == "http://json-schema.org/draft-07/schema#"
return data
119 changes: 119 additions & 0 deletions tests/test_additional_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import pytest
from marshmallow import Schema, fields

from marshmallow_jsonschema import UnsupportedValueError, JSONSchema
from marshmallow_jsonschema.compat import RAISE, INCLUDE, EXCLUDE
from . import validate_and_dump


def test_additional_properties_default():
class TestSchema(Schema):
foo = fields.Integer()

schema = TestSchema()

dumped = validate_and_dump(schema)

assert not dumped["definitions"]["TestSchema"]["additionalProperties"]


@pytest.mark.parametrize("additional_properties_value", (False, True))
def test_additional_properties_from_meta(additional_properties_value):
class TestSchema(Schema):
class Meta:
additional_properties = additional_properties_value

foo = fields.Integer()

schema = TestSchema()

dumped = validate_and_dump(schema)

assert (
dumped["definitions"]["TestSchema"]["additionalProperties"]
== additional_properties_value
)


def test_additional_properties_invalid_value():
class TestSchema(Schema):
class Meta:
additional_properties = "foo"

foo = fields.Integer()

schema = TestSchema()
json_schema = JSONSchema()

with pytest.raises(UnsupportedValueError):
json_schema.dump(schema)


def test_additional_properties_nested_default():
class TestNestedSchema(Schema):
foo = fields.Integer()

class TestSchema(Schema):
nested = fields.Nested(TestNestedSchema())

schema = TestSchema()

dumped = validate_and_dump(schema)

assert not dumped["definitions"]["TestSchema"]["additionalProperties"]


@pytest.mark.parametrize("additional_properties_value", (False, True))
def test_additional_properties_from_nested_meta(additional_properties_value):
class TestNestedSchema(Schema):
class Meta:
additional_properties = additional_properties_value

foo = fields.Integer()

class TestSchema(Schema):
nested = fields.Nested(TestNestedSchema())

schema = TestSchema()

dumped = validate_and_dump(schema)

assert (
dumped["definitions"]["TestNestedSchema"]["additionalProperties"]
== additional_properties_value
)


@pytest.mark.parametrize(
"unknown_value, additional_properties",
((RAISE, False), (INCLUDE, True), (EXCLUDE, False)),
)
def test_additional_properties_deduced(unknown_value, additional_properties):
class TestSchema(Schema):
class Meta:
unknown = unknown_value

foo = fields.Integer()

schema = TestSchema()

dumped = validate_and_dump(schema)

assert (
dumped["definitions"]["TestSchema"]["additionalProperties"]
== additional_properties
)


def test_additional_properties_unknown_invalid_value():
class TestSchema(Schema):
class Meta:
unknown = "foo"

foo = fields.Integer()

schema = TestSchema()
json_schema = JSONSchema()

with pytest.raises(UnsupportedValueError):
json_schema.dump(schema)
Loading

0 comments on commit 87a66c1

Please sign in to comment.