Skip to content

Commit

Permalink
Add support for title and description metadata
Browse files Browse the repository at this point in the history
  - Refactor _resolve_additional_properties to reuse parts of it for
  title and description support
  - Add _get_schema_metadata that retrieves the 3 currently supported
  metadata options on a schema
  - Rename 'cls' params to something less confusing as 'cls' is a python
  keyword
  - Add documentation for title, description and additional_properties
  - Move addtional_properties tests to a new forlder
  "schema_metadata_tests" so they can be gathered in a common place

Closes fuhrysteve#41
  • Loading branch information
Luke Marlin committed Oct 30, 2019
1 parent 3c579c7 commit b12e040
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 16 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,45 @@ 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()
```

#### Customizing a field
When defining a `field` in a `Schema`, it is possible to override the `field`'s JSONSchema properties using the `metadata` param.
For example, to give a description to a field:

```python
class MySchema(Schema):
myfield = fields.String(metadata={'description': 'My description'})
```

As noted before, this also allows overriding as well as providing additional properties:

```python
>>>class MySchema(Schema):
... myfield = fields.String(metadata={'unkown_prop':'value', "type":"integer"}, required=True)
>>>JSONSchema().dump(MySchema())
{'$schema': 'http://json-schema.org/draft-07/schema#', 'definitions': {'MySchema': {'properties': {'myfield': {'title': 'myfield', 'type': 'integer', 'unkown_prop': 'value'}}, 'required': ['myfield'], 'type': 'object', 'additionalProperties': False}}, '$ref': '#/definitions/MySchema'}
```

In the above example, we added some unspecified field AND we also changed the type entirely.


#### 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

0 comments on commit b12e040

Please sign in to comment.