Repository: yukinarit/pyserde
Pull request #118 (2021-04-23 16:18:35)

feat: Improve error description for union type
It's convenient if you can see what error each type in union received during deserialization.

Given this class,

```python
@deserialize
@serialize
@dataclass
class A:
    v: Union[IPv4Address, UUID]
```

Previously, this kind of error description are returned.

```
Can not deserialize 'not-ip-or-uuid' of type str into Union[IPv4Address, UUID]. Reason: badly formed hexadecimal UUID string
```

However, it's impossible to know why the type IPv4Address failed.

With this change, you can receive the following description instead.

```
Can not deserialize 'not-ip-or-uuid' of type str into Union[IPv4Address, UUID]
Reason:
Exhausted all types
 Failed to deserialize into IPv4Address: Expected 4 octets in 'not-ip-or-uuid
 Failed to deserialize into UUID: badly formed hexadecimal UUID string
```

github-actions[bot]: Please consider formatting your code according to the standards described here: https://github.com/yukinarit/pyserde/blob/master/CONTRIBUTING.md
codecov[bot]: # [Codecov](https://codecov.io/gh/yukinarit/pyserde/pull/118?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit) Report
> Merging [#118](https://codecov.io/gh/yukinarit/pyserde/pull/118?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit) (c62183e) into [master](https://codecov.io/gh/yukinarit/pyserde/commit/cc16cb0fccd3ab029e610af754ab7e58ea4e65d7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit) (cc16cb0) will **not change** coverage.
> The diff coverage is `n/a`.

> :exclamation: Current head c62183e differs from pull request most recent head 6db920b. Consider uploading reports for the commit 6db920b to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/yukinarit/pyserde/pull/118/graphs/tree.svg?width=650&height=150&src=pr&token=fF9S7dP4eH&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit)](https://codecov.io/gh/yukinarit/pyserde/pull/118?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit)

```diff
@@           Coverage Diff           @@
##           master     #118   +/-   ##
=======================================
  Coverage   88.86%   88.86%           
=======================================
  Files          11       11           
  Lines         997      997           
  Branches      215      215           
=======================================
  Hits          886      886           
  Misses         72       72           
  Partials       39       39           
```


| [Impacted Files](https://codecov.io/gh/yukinarit/pyserde/pull/118?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit) | Coverage Δ | |
|---|---|---|
| [serde/de.py](https://codecov.io/gh/yukinarit/pyserde/pull/118/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit#diff-c2VyZGUvZGUucHk=) | `91.80% <ø> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/yukinarit/pyserde/pull/118?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/yukinarit/pyserde/pull/118?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit). Last update [cc16cb0...6db920b](https://codecov.io/gh/yukinarit/pyserde/pull/118?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=yukinarit).


Patch:
diff --git a/serde/de.py b/serde/de.py
index a2ea988..55d991a 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -646,21 +646,21 @@ def {{func}}(data, reuse_instances):
   # create fake dict so we can reuse the normal render function
   fake_dict = {"fake_key":data}
 
-  error = "Exhausted all types"
+  errors = []
   {% for t in union_args %}
   {% if t | is_primitive or t | is_none %}
   if isinstance(data, {{t.__name__}}):
     return {{t|arg|rvalue}}
   else:
-    error = "input is not of type {{t.__name__}}"
+    errors.append("input is not of type {{t.__name__}}")
   {% else %}
   try:
     return {{t|arg|rvalue}}
   except Exception as e:
-    error = str(e)
+    errors.append(f' Failed to deserialize into {{t.__name__}}: {e}')
   {% endif %}
   {% endfor %}
-  raise SerdeError("Can not deserialize " + repr(data) + " of type " + typename(type(data)) + " into {{union_name}}. Reason: " + error)
+  raise SerdeError("Can not deserialize " + repr(data) + " of type " + typename(type(data)) + " into {{union_name}}.\\nReasons:\\n" + "\\n".join(errors))
     """
     union_name = f"Union[{', '.join([typename(a) for a in union_args])}]"
 


Test patch:
diff --git a/tests/test_union.py b/tests/test_union.py
index 9e8b188..1d4a24a 100644
--- a/tests/test_union.py
+++ b/tests/test_union.py
@@ -211,24 +211,26 @@ def test_union_exception_if_nothing_matches():
 
     with pytest.raises(SerdeError) as ex1:
         from_dict(A, {"v": "not-ip-or-uuid"})
-    assert (
-        str(ex1.value)
-        == "Can not deserialize 'not-ip-or-uuid' of type str into Union[IPv4Address, UUID]. Reason: badly formed hexadecimal UUID string"
+    assert str(ex1.value) == (
+        "Can not deserialize 'not-ip-or-uuid' of type str into Union[IPv4Address, UUID].\n"
+        "Reasons:\n"
+        " Failed to deserialize into IPv4Address: Expected 4 octets in 'not-ip-or-uuid'\n"
+        " Failed to deserialize into UUID: badly formed hexadecimal UUID string"
     )
 
     with pytest.raises(SerdeError) as ex2:
         from_dict(A, {"v": "not-ip-or-uuid"}, reuse_instances=True)
-    assert (
-        str(ex2.value)
-        == "Can not deserialize 'not-ip-or-uuid' of type str into Union[IPv4Address, UUID]. Reason: badly formed hexadecimal UUID string"
+    assert str(ex2.value) == (
+        "Can not deserialize 'not-ip-or-uuid' of type str into Union[IPv4Address, UUID].\n"
+        "Reasons:\n"
+        " Failed to deserialize into IPv4Address: Expected 4 octets in 'not-ip-or-uuid'\n"
+        " Failed to deserialize into UUID: badly formed hexadecimal UUID string"
     )
 
     with pytest.raises(SerdeError) as ex3:
         from_dict(A, {"v": None})
     # omit reason because it is not the same for all python versions & operating systems
-    assert str(ex3.value).startswith(
-        "Can not deserialize None of type NoneType into Union[IPv4Address, UUID]. Reason: "
-    )
+    assert str(ex3.value).startswith("Can not deserialize None of type NoneType into Union[IPv4Address, UUID].")
 
     with pytest.raises(SerdeError) as ex4:
         to_dict(A("not-ip-or-uuid"))