Repository: s-knibbs/dataclasses-jsonschema (python)

Issue:
Support tuple types
Tuples are not currently supported however JSON schema does support tuple validation:
https://json-schema.org/understanding-json-schema/reference/array.html#tuple-validation

Pull request description:
Add support for tuple types, using JSON schema tuple validation
Generate correct schema for integer & float types

Fixes #7 

Patch:
diff --git a/README.rst b/README.rst
index cad5185..d4ba56b 100644
--- a/README.rst
+++ b/README.rst
@@ -51,6 +51,7 @@ Deserialise data:
     >>> Point.from_dict({'x': 3.14, y: 'wrong'})
     jsonschema.exceptions.ValidationError: 'wrong' is not of type 'number'
 
+For more examples `see the tests <https://github.com/s-knibbs/dataclasses-jsonschema/blob/master/tests/conftest.py>`_
 
 TODO
 ----
diff --git a/dataclasses_jsonschema/__init__.py b/dataclasses_jsonschema/__init__.py
index e5979bc..9e45fa4 100644
--- a/dataclasses_jsonschema/__init__.py
+++ b/dataclasses_jsonschema/__init__.py
@@ -14,9 +14,9 @@ except ImportError:
 
 JSON_ENCODABLE_TYPES = {
     str: {'type': 'string'},
-    int: {'type': 'number', 'format': 'integer'},
+    int: {'type': 'integer'},
     bool: {'type': 'boolean'},
-    float: {'type': 'number', 'format': 'float'}
+    float: {'type': 'number'}
 }
 
 JsonEncodable = Union[int, float, str, bool]
@@ -137,13 +137,15 @@ class JsonSchemaMixin:
                         self._encode_field(ft.__args__[0], k, o): self._encode_field(ft.__args__[1], v, o)
                         for k, v in val.items()
                     }
-            elif field_type_name in ('Sequence', 'List'):
-                def encoder(ft, val, o): return type(val)(self._encode_field(ft.__args__[0], v, o) for v in val)
+            elif field_type_name in ('Sequence', 'List') or (field_type_name == "Tuple" and ... in field_type.__args__):
+                def encoder(ft, val, o): return [self._encode_field(ft.__args__[0], v, o) for v in val]
+            elif field_type_name == 'Tuple':
+                def encoder(ft, val, o):
+                    return [self._encode_field(ft.__args__[idx], v, o) for idx, v in enumerate(val)]
             elif self._is_json_schema_subclass(field_type):
                 # Only need to validate at the top level
                 def encoder(_, v, o): return v.to_dict(omit_none=o, validate=False)
             else:
-                # TODO: Copy value?
                 def encoder(_, v, __): return v
             self.__class__._encode_cache[field_type] = encoder  # type: ignore
         return encoder(field_type, value, omit_none)
@@ -192,9 +194,14 @@ class JsonSchemaMixin:
             elif field_type_name in ('Mapping', 'Dict'):
                 def decoder(f, ft, val, valid):
                     return {k: cls._decode_field(f, ft.__args__[1], v, valid) for k, v in val.items()}
-            elif field_type_name in ('Sequence', 'List'):
+            elif field_type_name in ('Sequence', 'List') or (field_type_name == "Tuple" and ... in field_type.__args__):
+                seq_type = tuple if field_type_name == "Tuple" else list
+
+                def decoder(f, ft, val, valid):
+                    return seq_type(cls._decode_field(f, ft.__args__[0], v, valid) for v in val)
+            elif field_type_name == "Tuple":
                 def decoder(f, ft, val, valid):
-                    return [cls._decode_field(f, ft.__args__[0], v, valid) for v in val]
+                    return tuple(cls._decode_field(f, ft.__args__[idx], v, valid) for idx, v in enumerate(val))
             elif hasattr(field_type, "__supertype__"):  # NewType field
                 def decoder(f, ft, val, valid):
                     return cls._decode_field(f, ft.__supertype__, val, valid)
@@ -255,10 +262,17 @@ class JsonSchemaMixin:
                 field_schema = {'type': 'object'}
                 if field_type.__args__[1] is not Any:
                     field_schema['additionalProperties'] = cls._get_field_schema(field_type.__args__[1])[0]
-            elif field_type_name in ('Sequence', 'List'):
+            elif field_type_name in ('Sequence', 'List') or (field_type_name == "Tuple" and ... in field_type.__args__):
                 field_schema = {'type': 'array'}
                 if field_type.__args__[0] is not Any:
                     field_schema['items'] = cls._get_field_schema(field_type.__args__[0])[0]
+            elif field_type_name == "Tuple":
+                tuple_len = len(field_type.__args__)
+                # TODO: How do we handle Optional type within lists / tuples
+                field_schema = {
+                    'type': 'array', 'minItems': tuple_len, 'maxItems': tuple_len,
+                    'items': [cls._get_field_schema(type_arg)[0] for type_arg in field_type.__args__]
+                }
             elif field_type in JSON_ENCODABLE_TYPES:
                 field_schema = JSON_ENCODABLE_TYPES[field_type]
             elif field_type in cls._field_encoders:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index ea5a0d4..ef5fa44 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,4 @@
-from typing import Optional, List, Dict, NewType, Any
+from typing import Optional, List, Dict, NewType, Any, Tuple
 from dataclasses import dataclass
 from enum import Enum
 from datetime import datetime
@@ -43,6 +43,8 @@ class Foo(JsonSchemaMixin):
     b: List[Point]
     c: Dict[str, int]
     d: Weekday
+    f: Tuple[str, int]
+    g: Tuple[str, ...]
     e: Optional[Postcode] = None
 
 
diff --git a/tests/test_all.py b/tests/test_all.py
index 6f50eef..d300eba 100644
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -13,11 +13,13 @@ FOO_SCHEMA = {
     'properties': {
         'a': {'format': 'date-time', 'type': 'string'},
         'b': {'items': {'$ref': '#/definitions/Point'}, 'type': 'array'},
-        'c': {'additionalProperties': {'format': 'integer', 'type': 'number'}, 'type': 'object'},
+        'c': {'additionalProperties': {'type': 'integer'}, 'type': 'object'},
         'd': {'type': 'string', 'enum': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']},
+        'f': {'type': 'array', 'minItems': 2, 'maxItems': 2, 'items': [{'type': 'string'}, {'type': 'integer'}]},
+        'g': {'type': 'array', 'items': {'type': 'string'}},
         'e': {'type': 'string', 'minLength': 5, 'maxLength': 8}},
     'type': 'object',
-    'required': ['a', 'b', 'c', 'd']
+    'required': ['a', 'b', 'c', 'd', 'f', 'g']
 }
 
 # Fixme: Fields in description no longer match
@@ -25,8 +27,8 @@ POINT_SCHEMA = {
     'description': Point.__doc__,
     'type': 'object',
     'properties': {
-        'z': {'format': 'float', 'type': 'number'},
-        'y': {'format': 'float', 'type': 'number'}
+        'z': {'type': 'number'},
+        'y': {'type': 'number'}
     },
     'required': ['z', 'y']
 }
@@ -76,9 +78,13 @@ def test_serialise_deserialise():
         'b': [{'z': 1.2, 'y': 1.5}],
         'c': {'Mon': 1, 'Tue': 2},
         'd': 'Wednesday',
-        'e': 'testing'
+        'e': 'testing',
+        'f': ['xyz', 6],
+        'g': ['abc']
     }
     f = Foo.from_dict(data)
+    assert f.f == ('xyz', 6)
+    assert f.g == ('abc',)
     assert data == f.to_dict()
 
 
@@ -94,7 +100,9 @@ def test_newtype_field_validation():
             'b': [{'z': 1.2, 'y': 1.5}],
             'c': {'Mon': 1, 'Tue': 2},
             'd': 'Wednesday',
-            'e': 't'
+            'e': 't',
+            'f': ['xyz', 6],
+            'g': ['abc']
         })