Skip to content
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

Add support for title and description metadata #99

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ if __name__ == '__main__':


### Advanced usage
#### Adding metadata on schemas
It is possible to define metadata on schema definition.
So far, three of them are supported:
* description
* title
* additional_properties (results in additionalProperties when dumped)

To use them, you need to provide a `Meta` inner class to your `Schema` with the respective properties:

```python
class MySchema(Schema):
class Meta:
additional_properties = True
title = "A nice title"
description = "A lengthy description"
a_field = fields.String()
```

#### Custom Type support

Simply add a `_jsonschema_type_mapping` method to your field
Expand Down
65 changes: 50 additions & 15 deletions marshmallow_jsonschema/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,56 @@
}


def _resolve_additional_properties(cls):
meta = cls.Meta
def _get_schema_metadata(schema_cls):
metadata = {}

additional_properties = getattr(meta, "additional_properties", None)
if additional_properties is not None:
if additional_properties in (True, False):
return additional_properties
else:
metadata["additionalProperties"] = \
_resolve_additional_properties(schema_cls)

title = _resolve_metadata(schema_cls, "title", type=str)
if title:
metadata["title"] = title

description = _resolve_metadata(schema_cls, "description", type=str)
if description:
metadata["description"] = description

return metadata


def _resolve_metadata(schema_cls, name, type=None, acceptable_values=None):
meta = schema_cls.Meta

metadata = getattr(meta, name, None)
if metadata is None:
return metadata

if type is not None:
if not isinstance(metadata, type):
raise UnsupportedValueError(
"`additional_properties` must be either True or False"
"`{}` must be of type {}".format(name, type.__name__)
)

unknown = getattr(meta, "unknown", None)
if acceptable_values is not None:
if metadata not in acceptable_values:
raise UnsupportedValueError(
"`{}` must be one of the following values: {}".format(
name,
", ".join([str(v) for v in acceptable_values])
)
)

return metadata


def _resolve_additional_properties(schema_cls):
additional_properties = _resolve_metadata(
schema_cls, "additional_properties", type=bool)

if additional_properties is not None:
return additional_properties

unknown = getattr(schema_cls.Meta, "unknown", None)
if unknown is None:
return False
elif unknown in (RAISE, EXCLUDE):
Expand Down Expand Up @@ -204,9 +241,7 @@ def _from_nested_schema(self, obj, field):
wrapped_nested.dump(nested_instance)
)

wrapped_dumped["additionalProperties"] = _resolve_additional_properties(
nested_cls
)
wrapped_dumped.update(_get_schema_metadata(nested_cls))

self._nested_schema_classes[name] = wrapped_dumped

Expand Down Expand Up @@ -243,10 +278,10 @@ def wrap(self, data, **_):
if self.nested: # no need to wrap, will be in outer defs
return data

cls = self.obj.__class__
name = cls.__name__
schema_cls = self.obj.__class__
name = schema_cls.__name__

data["additionalProperties"] = _resolve_additional_properties(cls)
data.update(_get_schema_metadata(schema_cls))

self._nested_schema_classes[name] = data
root = {
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

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


def test_additional_properties_default():
Expand Down
102 changes: 102 additions & 0 deletions tests/schema_metadata_tests/test_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import pytest
from marshmallow import Schema, fields

from marshmallow_jsonschema import UnsupportedValueError, JSONSchema
from .. import validate_and_dump


class TestDescriptionSchemaMetadata:

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

schema = TestSchema()
dumped = validate_and_dump(schema)
definition_to_test = dumped["definitions"]["TestSchema"]
assert "description" not in definition_to_test

@pytest.mark.parametrize("description_value", ("desc1", "desc2"))
def test_description_from_meta(self, description_value):
class TestSchema(Schema):
class Meta:
description = description_value
foo = fields.Integer()

schema = TestSchema()
dumped = validate_and_dump(schema)

assert (
dumped["definitions"]["TestSchema"]["description"]
== description_value
)

@pytest.mark.parametrize("invalid_value", [
True,
4,
{}
])
def test_description_invalid_value(self, invalid_value):
class TestSchema(Schema):
class Meta:
description = invalid_value
foo = fields.Integer()

schema = TestSchema()
json_schema = JSONSchema()

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


class TestDescriptionNestedSchemaMetadata:

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

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

schema = TestSchema()
dumped = validate_and_dump(schema)
definition_to_test = dumped["definitions"]["TestNestedSchema"]
assert "description" not in definition_to_test

@pytest.mark.parametrize("description_value", ("desc1", "desc2"))
def test_description_from_meta(self, description_value):
class TestNestedSchema(Schema):
class Meta:
description = description_value
foo = fields.Integer()

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

schema = TestSchema()
dumped = validate_and_dump(schema)

assert (
dumped["definitions"]["TestNestedSchema"]["description"]
== description_value
)

@pytest.mark.parametrize("invalid_value", [
True,
4,
{}
])
def test_description_invalid_value(self, invalid_value):
class TestNestedSchema(Schema):
class Meta:
description = invalid_value
foo = fields.Integer()

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

schema = TestSchema()
json_schema = JSONSchema()

with pytest.raises(UnsupportedValueError):
json_schema.dump(schema)
102 changes: 102 additions & 0 deletions tests/schema_metadata_tests/test_title.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import pytest
from marshmallow import Schema, fields

from marshmallow_jsonschema import UnsupportedValueError, JSONSchema
from .. import validate_and_dump


class TestDescriptionSchemaMetadata:

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

schema = TestSchema()
dumped = validate_and_dump(schema)
definition_to_test = dumped["definitions"]["TestSchema"]
assert "title" not in definition_to_test

@pytest.mark.parametrize("title_value", ("desc1", "desc2"))
def test_title_from_meta(self, title_value):
class TestSchema(Schema):
class Meta:
title = title_value
foo = fields.Integer()

schema = TestSchema()
dumped = validate_and_dump(schema)

assert (
dumped["definitions"]["TestSchema"]["title"]
== title_value
)

@pytest.mark.parametrize("invalid_value", [
True,
4,
{}
])
def test_title_invalid_value(self, invalid_value):
class TestSchema(Schema):
class Meta:
title = invalid_value
foo = fields.Integer()

schema = TestSchema()
json_schema = JSONSchema()

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


class TestDescriptionNestedSchemaMetadata:

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

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

schema = TestSchema()
dumped = validate_and_dump(schema)
definition_to_test = dumped["definitions"]["TestNestedSchema"]
assert "title" not in definition_to_test

@pytest.mark.parametrize("title_value", ("desc1", "desc2"))
def test_title_from_meta(self, title_value):
class TestNestedSchema(Schema):
class Meta:
title = title_value
foo = fields.Integer()

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

schema = TestSchema()
dumped = validate_and_dump(schema)

assert (
dumped["definitions"]["TestNestedSchema"]["title"]
== title_value
)

@pytest.mark.parametrize("invalid_value", [
True,
4,
{}
])
def test_title_invalid_value(self, invalid_value):
class TestNestedSchema(Schema):
class Meta:
title = invalid_value
foo = fields.Integer()

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

schema = TestSchema()
json_schema = JSONSchema()

with pytest.raises(UnsupportedValueError):
json_schema.dump(schema)
22 changes: 22 additions & 0 deletions tests/schema_metadata_tests/test_unhandled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest
from marshmallow import Schema, fields

from marshmallow_jsonschema import JSONSchema
from .. import validate_and_dump


@pytest.mark.parametrize("unchecked_value", (False, True))
def test_unhandled_metas_do_not_pollute_schema(unchecked_value):

class TestSchema(Schema):
class Meta:
unhandled = unchecked_value

foo = fields.Integer()

schema = TestSchema()
dumped = validate_and_dump(schema)

definition_to_check = dumped["definitions"]["TestSchema"]

assert "unhandled" not in definition_to_check