Repository: s-knibbs/dataclasses-jsonschema (2019-08-10 ~ 2019-09-10)

problem_statement: Add to_json and from_json convenience methods
It'd be nice to add `from_json` and `to_json` methods for convenience
date: 2019-08-10
patch: diff --git a/dataclasses_jsonschema/__init__.py b/dataclasses_jsonschema/__init__.py
index b17098d..a139823 100644
--- a/dataclasses_jsonschema/__init__.py
+++ b/dataclasses_jsonschema/__init__.py
@@ -1,3 +1,4 @@
+import json
 import sys
 import functools
 from _decimal import Decimal
@@ -730,3 +731,10 @@ class JsonSchemaMixin:
             # The types in the 'typing' module lack the __name__ attribute
             match = re.match(r'typing\.([A-Za-z]+)', str(field_type))
             return str(field_type) if match is None else match.group(1)
+
+    @classmethod
+    def from_json(cls: Type[T], data: str, validate: bool = True, **json_kwargs) -> T:
+        return cls.from_dict(json.loads(data, **json_kwargs), validate)
+
+    def to_json(self, omit_none: bool = True, validate: bool = False, **json_kwargs) -> str:
+        return json.dumps(self.to_dict(omit_none, validate), **json_kwargs)

test_patch: diff --git a/tests/test_core.py b/tests/test_core.py
index 793e572..51ac7df 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -219,6 +219,41 @@ def test_serialise_deserialise():
     assert data == f.to_dict()
 
 
+def test_from_json_to_json():
+    data = """
+{
+  "a": "2018-06-03T12:00:00+00:00",
+  "b": [
+    {
+      "z": 1.2,
+      "y": 1.5
+    }
+  ],
+  "c": {
+    "Mon": 1,
+    "Tue": 2
+  },
+  "d": "Wednesday",
+  "f": [
+    "xyz",
+    6
+  ],
+  "g": [
+    "abc"
+  ],
+  "e": "testing",
+  "h": {
+    "z": 0.5,
+    "y": 1.0
+  }
+}
+"""
+    f = Foo.from_json(data)
+    assert f.f == ('xyz', 6)
+    assert f.g == ('abc',)
+    assert data.strip() == f.to_json(indent=2)
+
+
 def test_invalid_data():
     with pytest.raises(ValidationError):
         Point.from_dict({'z': 3.14, 'y': 'wrong'})


----

problem_statement: Any type should generate an empty field schema
The following definition:

```py
@dataclass
class Parameter(JsonSchemaMixin):

    id: str
    type: ParameterTypeEnum
    value: Any
```

leads to schema like this:

```py
{'type': 'object', 'required': ['id', 'type', 'value'], 'properties': {'id': {'type': 'string'}, 'type': {'type': 'string', 'enum': ['string', 'double']}, 'value': {'type': 'object'}}}
```

But I would rather see:

```py
{'type': 'object', 'required': ['id', 'type', 'value'], 'properties': {'id': {'type': 'string'}, 'type': {'type': 'string', 'enum': ['string', 'double']}, 'value': {}}}
```

...where ```type``` for ```value``` is ommited, which means that ```value``` could be data of any type (string or number in this case). Is there a way how to achieve it? Many thanks.
date: 2019-08-17
patch: diff --git a/dataclasses_jsonschema/__init__.py b/dataclasses_jsonschema/__init__.py
index a139823..929ee6d 100644
--- a/dataclasses_jsonschema/__init__.py
+++ b/dataclasses_jsonschema/__init__.py
@@ -527,7 +527,7 @@ class JsonSchemaMixin:
 
     @classmethod
     def _get_field_schema(cls, field: Union[Field, Type], schema_type: SchemaType) -> Tuple[JsonDict, bool]:
-        field_schema: JsonDict = {'type': 'object'}
+        field_schema: JsonDict = {}
         required = True
 
         if isinstance(field, Field):

test_patch: diff --git a/tests/test_core.py b/tests/test_core.py
index 51ac7df..0cbcb3d 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -717,3 +717,23 @@ def test_discriminators():
     }
     assert Dog(name="Fido", breed="Dalmation").to_dict() == expected_dog
     assert Dog(name="Fido", breed="Dalmation") == Pet.from_dict(expected_dog)
+from typing import List, NewType, Optional, Union, Set, Any
+
+
+def test_any_type_schema():
+    @dataclass
+    class GraphNode(JsonSchemaMixin):
+        """A graph node"""
+        id: int
+        data: Any
+
+    expected_schema = compose_schema({
+        "type": "object",
+        "description": "A graph node",
+        "properties": {
+            "id": {"type": "integer"},
+            "data": {}
+        },
+        "required": ["id", "data"]
+    })
+    assert GraphNode.json_schema() == expected_schema
\ No newline at end of file


----

problem_statement: don't allow undeclared object properties
Thanks for the great work on this project, been really useful thus far.

We used `"additionalProperties": false` for objects in our (hand-written) json-schema's, which was helpful to prevent invalid inputs. In this project I see no way of achieving the same.

```python
from dataclasses import dataclass
from dataclasses_jsonschema import JsonSchemaMixin

@dataclass
class Point(JsonSchemaMixin):
    "A 2D point"
    x: float
    y: float

# adding an additional key to the dict results in the key being dropped
print(Point.from_dict({"x": 1.1, "y": 2.2, "z": 3.3}))
Point(x=1.1, y=2.2)
```

This is especially problematic with items that have defaults, if a field name is typod validation will still pass, but the resulting object will have unexpected values.

desired validation can be achieved by updating `dataclass_jsonschema/__init__.py` on line 690:
```python
schema = {
                'type': 'object',
                'required': required,
                'properties': properties,
                'additionalProperties': False
            }
```

```python
Point.from_dict({"x": 1.1, "y": 2.2, "z": 3.3})
Traceback (most recent call last):
  File "/Users/jmmccon/.pyenv/versions/taskcat-v9-3.6.9/lib/python3.6/site-packages/dataclasses_jsonschema/__init__.py", line 421, in _validate
    validate_func(data, cls.json_schema())
  File "/Users/jmmccon/.pyenv/versions/taskcat-v9-3.6.9/lib/python3.6/site-packages/jsonschema/validators.py", line 899, in validate
    raise error
jsonschema.exceptions.ValidationError: Additional properties are not allowed ('z' was unexpected)
```

I'm happy to pr a change, but not sure if I've missed some way to do this that is already catered for, or even if others want this behavior (One thought to make it optional is to provide an additional mixin `StrictJsonSchemaMixin` that adds this)
date: 2019-09-10
patch: diff --git a/dataclasses_jsonschema/__init__.py b/dataclasses_jsonschema/__init__.py
index 929ee6d..5b26215 100644
--- a/dataclasses_jsonschema/__init__.py
+++ b/dataclasses_jsonschema/__init__.py
@@ -183,12 +183,13 @@ class JsonSchemaMixin:
     __discriminator_name: Optional[str]
     # True if __discriminator_name is inherited from the base class
     __discriminator_inherited: bool
+    __allow_additional_props: bool
 
     @classmethod
     def _discriminator(cls) -> Optional[str]:
         return cls.__discriminator_name
 
-    def __init_subclass__(cls, discriminator: Optional[Union[str, bool]] = None):
+    def __init_subclass__(cls, discriminator: Optional[Union[str, bool]] = None, allow_additional_props: bool = True):
         # Initialise caches
         cls.__schema = {}
         cls.__compiled_schema = {}
@@ -214,6 +215,7 @@ class JsonSchemaMixin:
                     cls.__discriminator_inherited = True
             else:
                 cls.__discriminator_name = None
+        cls.__allow_additional_props = allow_additional_props
 
     @classmethod
     def field_mapping(cls) -> Dict[str, str]:
@@ -686,6 +688,9 @@ class JsonSchemaMixin:
                 'properties': properties
             }
 
+            if not cls.__allow_additional_props:
+                schema["additionalProperties"] = False
+
             if cls.__discriminator_name is not None and \
                     schema_type == SchemaType.OPENAPI_3 and \
                     not cls.__discriminator_inherited:

test_patch: diff --git a/tests/test_core.py b/tests/test_core.py
index 0cbcb3d..b838680 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -736,4 +736,23 @@ def test_any_type_schema():
         },
         "required": ["id", "data"]
     })
-    assert GraphNode.json_schema() == expected_schema
\ No newline at end of file
+    assert GraphNode.json_schema() == expected_schema
+
+def test_additional_properties_allowed():
+    @dataclass
+    class Scorpion(JsonSchemaMixin, allow_additional_props=False):
+        """A scorpion"""
+        species: str
+        venom_rating: int
+
+    expected_schema = compose_schema({
+        "type": "object",
+        "description": "A scorpion",
+        "properties": {
+            "species": {"type": "string"},
+            "venom_rating": {"type": "integer"},
+        },
+        "required": ["species", "venom_rating"],
+        "additionalProperties": False,
+    })
+    assert Scorpion.json_schema() == expected_schema
\ No newline at end of file