Repository: lidatong/dataclasses-json

Issue:
Invalid input type
Hello,

The title shows the error i've been having with this library. I've put together a scenario which reproduces the problem:

```
from dataclasses import dataclass
from typing import List

from dataclasses_json import dataclass_json


@dataclass_json
@dataclass
class Relative:
    name: str

@dataclass_json
@dataclass
class Person:
    name: str
    relatives: List[Relative]


people_json = '[{"name": "lidatong", "relatives": [{"name": "relative 1"}, {"name": "relative 2"}]}]'
Person.schema().loads(people_json, many=True)

```

The stacktrace i get is:
```
Traceback (most recent call last):
  File "main.py", line 20, in <module>
    Person.schema().loads(people_json, many=True)
  File "/home/rakan/.pyenv/versions/proj/lib/python3.7/site-packages/marshmallow/schema.py", line 724, in loads
    return self.load(data, many=many, partial=partial, unknown=unknown)
  File "/home/rakan/.pyenv/versions/proj/lib/python3.7/site-packages/marshmallow/schema.py", line 695, in load
    postprocess=True,
  File "/home/rakan/.pyenv/versions/sharq-urgent/lib/python3.7/site-packages/marshmallow/schema.py", line 857, in _do_load
    raise exc
marshmallow.exceptions.ValidationError: {0: {'relatives': {'_schema': ['Invalid input type.']}}}
```
schema().dumps not working properly with field initializers
Consider the following dataclass:
```python
from typing import List

from dataclasses import dataclass, field
from dataclasses_json import dataclass_json

@dataclass_json
@dataclass
class Bar:
    foos: List[str] = field(default_factory=list)
```

using `Bar.schema().dumps(Bar())` gives me the following error:

```
Traceback (most recent call last):
  File "dataclass_test.py", line 20, in <module>
    Bar.schema().dumps(Bar())
  File "~/.venv/venv/lib/python3.6/site-packages/dataclasses_json/api.py", line 100, in schema
    infer_missing)
  File "~/.venv/venv/lib/python3.6/site-packages/dataclasses_json/mm.py", line 90, in _make_default_fields
    cls)
  File "~/.venv/venv/lib/python3.6/site-packages/dataclasses_json/mm.py", line 105, in _make_default_field
    type_arg = type_.___args__[0]
AttributeError: type object 'List' has no attribute '___args__'

Process finished with exit code 1
```

However, it works when the type of `foos` is a `List` of another `dataclass_json`, like this:
```python
@dataclass_json
@dataclass
class Foo:
    one: str


@dataclass_json
@dataclass
class Bar:
    foos: List[Foo] = field(default_factory=list)


if __name__ == "__main__":
    print(Bar.schema().dumps(Bar()))
```

So something seems to be off with types and field initializers

I am using `dataclasses-json==0.2.1`

Discussion:
dsvensson: This fixes cases like this, hooray!

```
class Bar:
 b: str

class Foo:
  a: List[Bar]
```

But if Bar contains an Enum-field, that fails with:
```
  File ".../venv/lib/python3.7/site-packages/dataclasses_json/api.py", line 76, in schema
    Schema = build_schema(cls, DataClassJsonMixin, infer_missing, partial)
  File ".../venv/lib/python3.7/site-packages/dataclasses_json/mm.py", line 113, in build_schema
    schema_ = schema(cls, mixin, infer_missing)
  File ".../venv/lib/python3.7/site-packages/dataclasses_json/mm.py", line 98, in schema
    t = build_type(type_, options, mixin, field, cls)
  File ".../venv/lib/python3.7/site-packages/dataclasses_json/mm.py", line 73, in build_type
    return inner(type_, options)
  File ".../venv/lib/python3.7/site-packages/dataclasses_json/mm.py", line 72, in inner
    return field.Field(**options)
AttributeError: 'Field' object has no attribute 'Field'
```
Peilonrayz: @dsvensson Looks like an easy fix, I used `field` rather than `fields` [here](https://github.com/lidatong/dataclasses-json/pull/66/files#diff-17d6f2f251ae64dcc26ae93bf8e23974R72).

Could you provide the code that made this error?
dsvensson: @Peilonrayz Oh that was easy. It was indeed just that typo. Too much redacting, but I can provide a test case later today. There's also another test case that ought to be added, for list of classes where one field contains a list of classes. That didn't work before this branch. #57 . More or less extending the last decoder test in the test...nestedsomething..py.
Peilonrayz: @dsvensson Yeah I had done that with another `fields.Fields`, thought I changed all of them, after that tho... :(

I've added the test you mention already - [test][0] & [object][1]. Note, you can reproduce this without the outer most datatype being a list. It's just that the library only allows decoding lists via `.schema`, otherwise `from_json` and other code doesn't make use the schema code.

  [0]: https://github.com/lidatong/dataclasses-json/pull/66/commits/463ea01090b0d6571bb56c403f09a994e724a39c#diff-e1bc6e780c4b78fd3a4a85bf7b8ae0acR18
  [1]: https://github.com/lidatong/dataclasses-json/pull/66/commits/463ea01090b0d6571bb56c403f09a994e724a39c#diff-753922789de93c054356e2f67368b4d9R219

Patch:
diff --git a/dataclasses_json/api.py b/dataclasses_json/api.py
index 7098e69..66b52fd 100644
--- a/dataclasses_json/api.py
+++ b/dataclasses_json/api.py
@@ -1,15 +1,10 @@
 import abc
 import json
-from dataclasses import fields
-from datetime import datetime
 from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union
 
-from marshmallow import Schema, post_load
-
-from dataclasses_json import mm
+from dataclasses_json.mm import build_schema
 from dataclasses_json.core import (_ExtendedEncoder, _asdict, _decode_dataclass,
-                                   _overrides, _issubclass_safe,
-                                   _override)
+                                   _overrides, _override)
 
 A = TypeVar('A')
 B = TypeVar('B')
@@ -78,50 +73,15 @@ class DataClassJsonMixin(abc.ABC):
                dump_only=(),
                partial=False,
                unknown=None):
-        Meta = type('Meta',
-                    (),
-                    {'fields': tuple(field.name for field in fields(cls))})
-
-        @post_load
-        def make_instance(self, kvs):
-            return _decode_dataclass(cls, kvs, partial)
-
-        overriden_fields = {k: v.mm_field
-                            for k, v in _overrides(cls).items()}
-        generated_fields = {field for field in fields(cls)
-                            if field.name not in overriden_fields}
-        nested_fields = mm._make_nested_fields(generated_fields,
-                                               DataClassJsonMixin,
-                                               infer_missing)
-        primitive_fields = [field for field in generated_fields
-                            if field.name not in nested_fields]
-        default_fields = mm._make_default_fields(primitive_fields,
-                                                 cls,
-                                                 infer_missing)
-
-        # datetime codec is different from what's specified by `fields.DateTime`
-        # so need to override what is inferred by `class Meta`
-        datetime_fields = {field.name: mm._TimestampField(cls)
-                           for field in primitive_fields
-                           if _issubclass_safe(field.type, datetime)
-                           and field.name not in default_fields}
-
-        DataClassSchema = type(f'{cls.__name__.capitalize()}Schema',
-                               (Schema,),
-                               {'Meta': Meta,
-                                f'make_{cls.__name__.lower()}': make_instance,
-                                **overriden_fields,
-                                **nested_fields,
-                                **default_fields,
-                                **datetime_fields})
-        return DataClassSchema(only=only,
-                               exclude=exclude,
-                               many=many,
-                               context=context,
-                               load_only=load_only,
-                               dump_only=dump_only,
-                               partial=partial,
-                               unknown=unknown)
+        Schema = build_schema(cls, DataClassJsonMixin, infer_missing, partial)
+        return Schema(only=only,
+                      exclude=exclude,
+                      many=many,
+                      context=context,
+                      load_only=load_only,
+                      dump_only=dump_only,
+                      partial=partial,
+                      unknown=unknown)
 
 
 def dataclass_json(cls):
diff --git a/dataclasses_json/core.py b/dataclasses_json/core.py
index 3813fd5..ebc77ba 100644
--- a/dataclasses_json/core.py
+++ b/dataclasses_json/core.py
@@ -60,6 +60,8 @@ def _override(kvs, overrides, attr):
 
 
 def _decode_dataclass(cls, kvs, infer_missing):
+    if isinstance(kvs, cls):
+        return kvs
     overrides = _overrides(cls)
     kvs = {} if kvs is None and infer_missing else kvs
     missing_fields = {field for field in fields(cls) if field.name not in kvs}
diff --git a/dataclasses_json/mm.py b/dataclasses_json/mm.py
index 085cab6..ecd57cd 100644
--- a/dataclasses_json/mm.py
+++ b/dataclasses_json/mm.py
@@ -1,16 +1,18 @@
+import typing
 import warnings
-from dataclasses import MISSING, is_dataclass
+
+from dataclasses import MISSING, is_dataclass, fields as dc_fields
 from datetime import datetime
 from uuid import UUID
 
-from marshmallow import fields
+from marshmallow import fields, Schema, post_load
 
-from dataclasses_json.core import _is_supported_generic
-from dataclasses_json.utils import (_is_collection, _is_mapping,
-                                    _is_nonstr_collection, _is_optional,
+from dataclasses_json.core import _is_supported_generic, _decode_dataclass
+from dataclasses_json.utils import (_is_collection, _is_optional,
                                     _issubclass_safe, _timestamp_to_dt_aware)
 
 
+
 class _TimestampField(fields.Field):
     def _serialize(self, value, attr, obj, **kwargs):
         return value.timestamp()
@@ -27,7 +29,13 @@ class _IsoField(fields.Field):
         return datetime.fromisoformat(value)
 
 
-_type_to_cons = {
+TYPES = {
+    typing.Mapping: fields.Mapping,
+    typing.MutableMapping: fields.Mapping,
+    typing.List: fields.List,
+    typing.Dict: fields.Dict,
+    typing.Tuple: fields.Tuple,
+    typing.Callable: fields.Function,
     dict: fields.Dict,
     list: fields.List,
     str: fields.Str,
@@ -38,75 +46,75 @@ _type_to_cons = {
     UUID: fields.UUID
 }
 
-
-def _make_nested_fields(fields_, dataclass_json_cls, infer_missing):
-    nested_fields = {}
-    for field, type_, field_many in _inspect_nested_fields(fields_):
-        if _issubclass_safe(type_, dataclass_json_cls):
-            if infer_missing and _is_optional(field.type):
-                schema = fields.Nested(type_.schema(),
-                                       many=field_many,
-                                       missing=None)
+def build_type(type_, options, mixin, field, cls):
+    def inner(type_, options):
+        if is_dataclass(type_):
+            if _issubclass_safe(type_, mixin):
+                options['field_many'] = bool(_is_supported_generic(field.type) and _is_collection(field.type))
+                return fields.Nested(type_.schema(), **options)
             else:
-                schema = fields.Nested(type_.schema(),
-                                       many=field_many)
-            nested_fields[field.name] = schema
+                warnings.warn(f"Nested dataclass field {field.name} of type "
+                              f"{field.type} detected in "
+                              f"{cls.__name__} that is not an instance of "
+                              f"dataclass_json. Did you mean to recursively "
+                              f"serialize this field? If so, make sure to "
+                              f"augment {type_} with either the "
+                              f"`dataclass_json` decorator or mixin.")
+                return fields.Field(**options)
+
+        origin = getattr(type_, '__origin__', type_)
+        args = [inner(a, {}) for a in getattr(type_, '__args__', [])]
+
+        if origin in TYPES:
+            return TYPES[origin](*args, **options)
+        warnings.warn(f"Unknown type {type_} at {cls.__name__}.{field.name}: {field.type} "
+                      f"It's advised to pass the correct marshmallow type to `mm_field`.")
+        return field.Field(**options)
+    return inner(type_, options)
+
+
+def schema(cls, mixin, infer_missing):
+    schema = {}
+    for field in dc_fields(cls):
+        if 'dataclasses_json' in (field.metadata or {}):
+            schema[field.name] = field.metadata['dataclasses_json'].get('mm_field')
         else:
-            warnings.warn(f"Nested dataclass field {field.name} of type "
-                          f"{field.type} detected in "
-                          f"{dataclass_json_cls.__name__} that is not an instance of "
-                          f"dataclass_json. Did you mean to recursively "
-                          f"serialize this field? If so, make sure to "
-                          f"augment {field.type} with either the "
-                          f"`dataclass_json` decorator or mixin.")
-    return nested_fields
-
-
-def _inspect_nested_fields(fields_):
-    nested_dc_fields_and_is_many = []
-    for field in fields_:
-        if _is_supported_generic(field.type):
-            t_arg = field.type.__args__[0]
-            if is_dataclass(t_arg):
-                if _is_collection(field.type):
-                    nested_dc_fields_and_is_many.append((field, t_arg, True))
-                else:
-                    nested_dc_fields_and_is_many.append((field, t_arg, False))
-        elif is_dataclass(field.type):
-            nested_dc_fields_and_is_many.append((field, field.type, False))
-    return nested_dc_fields_and_is_many
-
-
-def _make_default_fields(fields_, cls, infer_missing):
-    default_fields = {}
-    for field in fields_:
-        if field.default is not MISSING:
-            default_fields[field.name] = _make_default_field(field.type,
-                                                             field.default,
-                                                             cls)
-        elif field.default_factory is not MISSING:
-            default_fields[field.name] = _make_default_field(field.type,
-                                                             field.default_factory,
-                                                             cls)
-        elif _is_optional(field.type) and infer_missing:
-            default_fields[field.name] = _make_default_field(field.type,
-                                                             None,
-                                                             cls)
-    return default_fields
-
-
-def _make_default_field(type_, default, cls):
-    cons_type = type_
-    cons_type = (list if _is_nonstr_collection(cons_type) else cons_type)
-    cons_type = (dict if _is_mapping(cons_type) else cons_type)
-    cons_type = (type_.__args__[0] if _is_optional(cons_type) else cons_type)
-    cons = _type_to_cons[cons_type]
-    if cons is fields.List:
-        type_arg = type_.___args__[0]
-        if type_arg not in _type_to_cons:
-            raise TypeError(f"Unsupported {type_arg} detected. Is it "
-                            f"a supported JSON type or dataclass_json "
-                            f"instance?")
-        arg_cons = _type_to_cons[type_arg]
-        return cons(cls, arg_cons, missing=default)
-    return cons(missing=default)
+            type_ = field.type
+            options = {}
+            missing_key = 'missing' if infer_missing else 'default'
+            if field.default is not MISSING:
+                options[missing_key] = field.default
+            elif field.default_factory is not MISSING:
+                options[missing_key] = field.default_factory
+
+            if options.get(missing_key, ...) is None:
+                options['allow_none'] = True
+
+            if _is_optional(type_):
+                options.setdefault(missing_key, None)
+                type_ = type_.__args__[0]
+                options['allow_none'] = True
+
+            t = build_type(type_, options, mixin, field, cls)
+            #if type(t) is not fields.Field:  # If we use `isinstance` we would return nothing.
+            schema[field.name] = t
+    return schema
+
+
+def build_schema(cls, mixin, infer_missing, partial):
+    Meta = type('Meta',
+                (),
+                {'fields': tuple(field.name for field in dc_fields(cls))})
+
+    @post_load
+    def make_instance(self, kvs):
+        return _decode_dataclass(cls, kvs, partial)
+
+    schema_ = schema(cls, mixin, infer_missing)
+    DataClassSchema = type(f'{cls.__name__.capitalize()}Schema',
+                           (Schema,),
+                           {'Meta': Meta,
+                            f'make_{cls.__name__.lower()}': make_instance,
+                            **schema_})
+
+    return DataClassSchema


Test patch:
diff --git a/tests/entities.py b/tests/entities.py
index d9e5428..14b96cf 100644
--- a/tests/entities.py
+++ b/tests/entities.py
@@ -1,5 +1,4 @@
 from dataclasses import dataclass, field
-from datetime import datetime
 from typing import (Collection,
                     Deque,
                     Dict,
@@ -15,7 +14,6 @@ from uuid import UUID
 from marshmallow import fields
 
 from dataclasses_json import DataClassJsonMixin, dataclass_json
-from dataclasses_json.mm import _IsoField
 
 A = TypeVar('A')
 
@@ -153,12 +151,6 @@ class DataClassJsonDecorator:
     x: str
 
 
-@dataclass_json
-@dataclass
-class DataClassWithDatetime:
-    created_at: datetime
-
-
 @dataclass_json
 @dataclass
 class DataClassWithOverride:
@@ -170,27 +162,36 @@ class DataClassWithOverride:
 
 @dataclass_json
 @dataclass
-class DataClassWithIsoDatetime:
-    created_at: datetime = field(
-        metadata={'dataclasses_json': {
-            'encoder': datetime.isoformat,
-            'decoder': datetime.fromisoformat,
-            'mm_field': fields.DateTime(format='iso')
-        }})
+class DataClassWithUuid:
+    id: UUID
+
 
 @dataclass_json
 @dataclass
-class DataClassWithCustomIsoDatetime:
-    created_at: datetime = field(
-        metadata={'dataclasses_json': {
-            'encoder': datetime.isoformat,
-            'decoder': datetime.fromisoformat,
-            'mm_field': _IsoField()
-        }})
+class DataClassDefaultListStr:
+    value: List[str] = field(default_factory=list)
+
 
+@dataclass_json
+@dataclass
+class DataClassChild:
+    name: str
 
 
 @dataclass_json
 @dataclass
-class DataClassWithUuid:
-    id: UUID
+class DataClassDefaultOptionalList:
+    children: Optional[List[DataClassChild]] = None
+
+
+@dataclass_json
+@dataclass
+class DataClassList:
+    children: List[DataClassChild]
+
+
+@dataclass_json
+@dataclass
+class DataClassOptional:
+    a: int
+    b: Optional[int]
diff --git a/tests/hypothesis2/strategies.py b/tests/hypothesis2/strategies.py
index 2ab565c..3b04566 100644
--- a/tests/hypothesis2/strategies.py
+++ b/tests/hypothesis2/strategies.py
@@ -3,7 +3,7 @@ from collections import deque
 from hypothesis.strategies import lists, none, one_of
 
 
-def deques(elements=None, min_size=None, average_size=None, max_size=None,
+def deques(elements=None, min_size=0, max_size=None,
            unique_by=None, unique=False):
     return lists(**locals()).map(deque)
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 6593f60..1075cce 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,14 +1,12 @@
-from datetime import datetime, timezone
 from uuid import UUID
 
 import pytest
 
 from tests.entities import (DataClassIntImmutableDefault, DataClassJsonDecorator,
-                            DataClassWithDataClass, DataClassWithDatetime,
-                            DataClassWithList, DataClassWithOptional,
-                            DataClassWithOptionalNested, DataClassWithUuid,
-                            DataClassWithIsoDatetime, DataClassWithOverride,
-                            DataClassWithCustomIsoDatetime, DataClassBoolImmutableDefault)
+                            DataClassWithDataClass, DataClassWithList,
+                            DataClassWithOptional, DataClassWithOptionalNested,
+                            DataClassWithUuid, DataClassWithOverride,
+                            DataClassBoolImmutableDefault)
 
 
 class TestTypes:
@@ -23,59 +21,6 @@ class TestTypes:
         assert (DataClassWithUuid.from_json(self.dc_uuid_json)
                 == DataClassWithUuid(UUID(self.uuid_s)))
 
-    dt = datetime(2018, 11, 17, 16, 55, 28, 456753, tzinfo=timezone.utc)
-    tz = timezone.utc
-
-    ts = dt.timestamp()
-    dc_ts_json = f'{{"created_at": {ts}}}'
-    dc_ts = DataClassWithDatetime(datetime.fromtimestamp(ts, tz=tz))
-
-    iso = dt.isoformat()
-    dc_iso_json = f'{{"created_at": "{iso}"}}'
-    dc_iso = DataClassWithIsoDatetime(datetime.fromisoformat(iso))
-
-    def test_datetime_encode(self):
-        assert (self.dc_ts.to_json() == self.dc_ts_json)
-
-    def test_datetime_decode(self):
-        assert (DataClassWithDatetime.from_json(self.dc_ts_json) == self.dc_ts)
-
-    def test_datetime_override_encode(self):
-        assert (self.dc_iso.to_json() == self.dc_iso_json)
-
-    def test_datetime_override_decode(self):
-        assert (DataClassWithIsoDatetime.from_json(
-            self.dc_iso_json) == self.dc_iso)
-
-    def test_datetime_schema_encode(self):
-        assert (DataClassWithDatetime.schema().dumps(self.dc_ts)
-                == self.dc_ts_json)
-
-    def test_datetime_schema_decode(self):
-        assert (DataClassWithDatetime.schema().loads(self.dc_ts_json)
-                == self.dc_ts)
-
-    def test_datetime_override_schema_encode(self):
-        assert (DataClassWithIsoDatetime.schema().dumps(self.dc_iso)
-                == self.dc_iso_json)
-
-    def test_datetime_override_schema_decode(self):
-        iso = DataClassWithIsoDatetime.schema().loads(self.dc_iso_json)
-        # FIXME bug in marshmallow currently returns datetime-naive instead of
-        # datetime-aware. also seems to drop microseconds?
-        # #955
-        iso.created_at = iso.created_at.replace(microsecond=456753,
-                                                tzinfo=self.tz)
-        assert (iso == self.dc_iso)
-
-    def test_datetime_custom_iso_fieldoverride_schema_encode(self):
-        assert (DataClassWithCustomIsoDatetime.schema().dumps(self.dc_iso)
-                == self.dc_iso_json)
-
-    def test_datetime_custom_iso_field_override_schema_decode(self):
-        iso = DataClassWithCustomIsoDatetime.schema().loads(self.dc_iso_json)
-        assert (iso == DataClassWithCustomIsoDatetime(self.dt))
-
 
 class TestInferMissing:
     def test_infer_missing(self):
diff --git a/tests/test_schema.py b/tests/test_schema.py
new file mode 100644
index 0000000..905e3b1
--- /dev/null
+++ b/tests/test_schema.py
@@ -0,0 +1,23 @@
+from .entities import DataClassDefaultListStr, DataClassDefaultOptionalList, DataClassList, DataClassOptional
+
+
+test_do_list = """[{}, {"children": [{"name": "a"}, {"name": "b"}]}]"""
+test_list = '[{"children": [{"name": "a"}, {"name": "b"}]}]'
+
+
+class TestSchema:
+    def test_default_list_str(self):
+        DataClassDefaultListStr.schema().dumps(DataClassDefaultListStr())
+        assert True
+
+    def test_default_optional_list(self):
+        DataClassDefaultOptionalList.schema().loads(test_do_list, many=True)
+        assert True
+
+    def test_list(self):
+        DataClassList.schema().loads(test_list, many=True)
+        assert True
+
+    def test_optional(self):
+        DataClassOptional.schema().loads('{"a": 4, "b": null}')
+        assert True
diff --git a/tests/test_time.py b/tests/test_time.py
new file mode 100644
index 0000000..d62c95c
--- /dev/null
+++ b/tests/test_time.py
@@ -0,0 +1,106 @@
+from datetime import datetime, timezone
+from dataclasses import dataclass, field
+import sys
+
+from marshmallow import fields
+import pytest
+
+from dataclasses_json import dataclass_json
+from dataclasses_json.mm import _IsoField
+
+
+@dataclass_json
+@dataclass
+class DataClassWithDatetime:
+    created_at: datetime
+
+
+if sys.version_info >= (3, 7):
+    @dataclass_json
+    @dataclass
+    class DataClassWithIsoDatetime:
+        created_at: datetime = field(
+            metadata={'dataclasses_json': {
+                'encoder': datetime.isoformat,
+                'decoder': datetime.fromisoformat,
+                'mm_field': fields.DateTime(format='iso')
+            }})
+
+
+    @dataclass_json
+    @dataclass
+    class DataClassWithCustomIsoDatetime:
+        created_at: datetime = field(
+            metadata={'dataclasses_json': {
+                'encoder': datetime.isoformat,
+                'decoder': datetime.fromisoformat,
+                'mm_field': _IsoField()
+            }})
+
+
+class TestTime:
+    dt = datetime(2018, 11, 17, 16, 55, 28, 456753, tzinfo=timezone.utc)
+    tz = timezone.utc
+
+    ts = dt.timestamp()
+    dc_ts_json = f'{{"created_at": {ts}}}'
+    dc_ts = DataClassWithDatetime(datetime.fromtimestamp(ts, tz=tz))
+
+    if sys.version_info >= (3, 7):
+        iso = dt.isoformat()
+        dc_iso_json = f'{{"created_at": "{iso}"}}'
+        dc_iso = DataClassWithIsoDatetime(datetime.fromisoformat(iso))
+
+    def test_datetime_encode(self):
+        assert (self.dc_ts.to_json() == self.dc_ts_json)
+
+    def test_datetime_decode(self):
+        assert (DataClassWithDatetime.from_json(self.dc_ts_json) == self.dc_ts)
+
+    @pytest.mark.skipif(sys.version_info < (3, 7),
+                        reason="requires python3.7")
+    def test_datetime_override_encode(self):
+        assert (self.dc_iso.to_json() == self.dc_iso_json)
+
+    @pytest.mark.skipif(sys.version_info < (3, 7),
+                        reason="requires python3.7")
+    def test_datetime_override_decode(self):
+        assert (DataClassWithIsoDatetime.from_json(
+            self.dc_iso_json) == self.dc_iso)
+
+    def test_datetime_schema_encode(self):
+        assert (DataClassWithDatetime.schema().dumps(self.dc_ts)
+                == self.dc_ts_json)
+
+    def test_datetime_schema_decode(self):
+        assert (DataClassWithDatetime.schema().loads(self.dc_ts_json)
+                == self.dc_ts)
+
+    @pytest.mark.skipif(sys.version_info < (3, 7),
+                        reason="requires python3.7")
+    def test_datetime_override_schema_encode(self):
+        assert (DataClassWithIsoDatetime.schema().dumps(self.dc_iso)
+                == self.dc_iso_json)
+
+    @pytest.mark.skipif(sys.version_info < (3, 7),
+                        reason="requires python3.7")
+    def test_datetime_override_schema_decode(self):
+        iso = DataClassWithIsoDatetime.schema().loads(self.dc_iso_json)
+        # FIXME bug in marshmallow currently returns datetime-naive instead of
+        # datetime-aware. also seems to drop microseconds?
+        # #955
+        iso.created_at = iso.created_at.replace(microsecond=456753,
+                                                tzinfo=self.tz)
+        assert (iso == self.dc_iso)
+
+    @pytest.mark.skipif(sys.version_info < (3, 7),
+                        reason="requires python3.7")
+    def test_datetime_custom_iso_fieldoverride_schema_encode(self):
+        assert (DataClassWithCustomIsoDatetime.schema().dumps(self.dc_iso)
+                == self.dc_iso_json)
+
+    @pytest.mark.skipif(sys.version_info < (3, 7),
+                        reason="requires python3.7")
+    def test_datetime_custom_iso_field_override_schema_decode(self):
+        iso = DataClassWithCustomIsoDatetime.schema().loads(self.dc_iso_json)
+        assert (iso == DataClassWithCustomIsoDatetime(self.dt))