Repository: ramonhagenaars/jsons

Issue:
Invalid type hints result in cryptic error messages
When a class has an invalid type hint, deserializing into that class may cause a ``DeserializationError`` that does not help solving the issue. The error should be more descriptive.

Example:

```python
import datetime  # <-- Note that the module is imported, not the class.
from dataclasses import dataclass
import jsons


@dataclass
class User:
    name: str
    birthday: datetime


dumped = { 'name': 'Albert', 'birthday': '1879-03-14T11:30:00+01:00' }
jsons.load(dumped, User)
```

This causes:

```python
Traceback (most recent call last):
  File "C:\Projects\Personal\jsons\jsons\_main_impl.py", line 128, in load
    return deserializer(json_obj, cls, **kwargs_)
  File "C:\Projects\Personal\jsons\jsons\deserializers\default_object.py", line 31, in default_object_deserializer
    constructor_args = _get_constructor_args(obj, cls, **kwargs)
  File "C:\Projects\Personal\jsons\jsons\deserializers\default_object.py", line 58, in _get_constructor_args
    constructor_args_in_obj = {key: value for key, value in args_gen if key}
  File "C:\Projects\Personal\jsons\jsons\deserializers\default_object.py", line 58, in <dictcomp>
    constructor_args_in_obj = {key: value for key, value in args_gen if key}
  File "C:\Projects\Personal\jsons\jsons\deserializers\default_object.py", line 57, in <genexpr>
    in signature_parameters.items() if sig_key != 'self')
  File "C:\Projects\Personal\jsons\jsons\deserializers\default_object.py", line 75, in _get_value_for_attr
    meta_hints, **kwargs)
  File "C:\Projects\Personal\jsons\jsons\deserializers\default_object.py", line 109, in _get_value_from_obj
    value = load(obj[sig_key], arg_cls, meta_hints=new_hints, **kwargs)
  File "C:\Projects\Personal\jsons\jsons\_main_impl.py", line 119, in load
    deserializer = _get_deserializer(cls, fork_inst)
  File "C:\Projects\Personal\jsons\jsons\_main_impl.py", line 145, in _get_deserializer
    fork_inst._classes_deserializers, fork_inst)
  File "C:\Projects\Personal\jsons\jsons\_main_impl.py", line 153, in _get_lizer
    cls_name = get_class_name(cls, str.lower, fork_inst=fork_inst)
  File "C:\Projects\Personal\jsons\jsons\_common_impl.py", line 51, in get_class_name
    module = _get_module(cls)
  File "C:\Projects\Personal\jsons\jsons\_common_impl.py", line 104, in _get_module
    module = cls.__module__
AttributeError: module 'datetime' has no attribute '__module__'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Projects/Personal/jsons/effe.py", line 14, in <module>
    jsons.load(dumped, User)
  File "C:\Projects\Personal\jsons\jsons\_main_impl.py", line 132, in load
    raise DeserializationError(str(err), json_obj, cls)
jsons.exceptions.DeserializationError: module 'datetime' has no attribute '__module__'
```

Discussion:

Patch:
diff --git a/jsons/_load_impl.py b/jsons/_load_impl.py
index 4cc1ffd..b8055b4 100644
--- a/jsons/_load_impl.py
+++ b/jsons/_load_impl.py
@@ -103,6 +103,9 @@ def _do_load(json_obj: object,
              cls: type,
              initial: bool,
              **kwargs):
+    cls_name = get_class_name(cls, fully_qualified=True)
+    if deserializer is None:
+        raise DeserializationError('No deserializer for type "{}"'.format(cls_name), json_obj, cls)
     try:
         result = deserializer(json_obj, cls, **kwargs)
         validate(result, cls, kwargs['fork_inst'])
@@ -110,7 +113,8 @@ def _do_load(json_obj: object,
         clear()
         if isinstance(err, JsonsError):
             raise
-        raise DeserializationError(str(err), json_obj, cls)
+        message = 'Could not deserialize value "{}" into "{}". {}'.format(json_obj, cls_name, err)
+        raise DeserializationError(message, json_obj, cls)
     else:
         if initial:
             # Clear all lru caches right before returning the initial call.
diff --git a/jsons/deserializers/default_primitive.py b/jsons/deserializers/default_primitive.py
index 3110904..2223957 100644
--- a/jsons/deserializers/default_primitive.py
+++ b/jsons/deserializers/default_primitive.py
@@ -17,6 +17,6 @@ def default_primitive_deserializer(obj: object,
         try:
             result = cls(obj)
         except ValueError:
-            raise DeserializationError('Could not cast {} into {}'
+            raise DeserializationError('Could not cast "{}" into "{}"'
                                        .format(obj, cls.__name__), obj, cls)
     return result
diff --git a/jsons/serializers/default_primitive.py b/jsons/serializers/default_primitive.py
index ee24f9c..6fa0a59 100644
--- a/jsons/serializers/default_primitive.py
+++ b/jsons/serializers/default_primitive.py
@@ -17,6 +17,6 @@ def default_primitive_serializer(obj: object,
         try:
             result = cls(obj)
         except ValueError:
-            raise SerializationError('Could not cast {} into {}'
+            raise SerializationError('Could not cast "{}" into "{}"'
                                      .format(obj, cls.__name__))
     return result


Test patch:
diff --git a/tests/test_type_error.py b/tests/test_type_error.py
new file mode 100644
index 0000000..2e12564
--- /dev/null
+++ b/tests/test_type_error.py
@@ -0,0 +1,43 @@
+import datetime
+from dataclasses import dataclass
+from unittest import TestCase
+import jsons
+from jsons import DeserializationError
+
+
+@dataclass
+class WrongUser:
+    id: int
+    birthday: datetime  # intentionally wrong
+
+
+@dataclass
+class CorrectUser:
+    id: int
+    birthday: datetime.datetime
+
+
+class TestTypeError(TestCase):
+    def test_undefined_deserializer(self):
+        dumped = {'id': 12, 'birthday': '1879-03-14T11:30:00+01:00'}
+        with self.assertRaises(DeserializationError) as errorContext:
+            jsons.load(dumped, WrongUser)
+        error: DeserializationError = errorContext.exception
+        self.assertEqual('No deserializer for type "datetime"', error.message)
+        self.assertEqual(datetime, error.target)
+
+    def test_wrong_primitive_type(self):
+        dumped = {'id': 'Albert', 'birthday': '1879-03-14T11:30:00+01:00'}
+        with self.assertRaises(DeserializationError) as errorContext:
+            jsons.load(dumped, WrongUser)
+        error: DeserializationError = errorContext.exception
+        self.assertEqual('Could not cast "Albert" into "int"', error.message)
+        self.assertEqual(int, error.target)
+
+    def test_wrong_type(self):
+        dumped = {'id': 12, 'birthday': 'every day'}
+        with self.assertRaises(DeserializationError) as errorContext:
+            jsons.load(dumped, CorrectUser)
+        error: DeserializationError = errorContext.exception
+        self.assertTrue(error.message.startswith('Could not deserialize value "every day" into "datetime.datetime".'))
+        self.assertEqual(datetime.datetime, error.target)