Repository: yukinarit/pyserde

Issue:
Error messages with more context
Errors encountered while deserialising don't provide much context on which part of a file or string is failing. This makes it hard to debug problems with files being parsed etc. A simple improvement would be to also provide the target type the value is being converted to. Ultimately providing the key (ideally full key path for nested structures) and it's value which is failing would be best for identifying the problem with a file.

```python
from dataclasses import dataclass
import typing
from typing import TypeVar

import serde
import serde.json

T = TypeVar("T")


@serde.serde(type_check=serde.Coerce)
@dataclass
class Foo:
    foo: str
    bar: int


if __name__ == "__main__":
    data = """
{
    "foo": "foo",
    "bar": "notint"
}
"""

    serde.json.from_json(Foo, data)
    # This will give `serde.compat.SerdeError: invalid literal for int() with base 10: 'notint'`
    # For a more complex json, this error message doesn't make it easy to find the offending field.

```

Discussion:

Patch:
diff --git a/README.md b/README.md
index a236d44..bc8cce1 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@
   <a href="https://codecov.io/gh/yukinarit/pyserde">
     <img alt="CodeCov" src="https://codecov.io/gh/yukinarit/pyserde/branch/main/graph/badge.svg">
   </a>
+    <a href = "https://piptrends.com/package/pyserde" alt = "pyserde Downloads Last Month"><img alt="pyserde Downloads Last Month by pip Trends" src="https://assets.piptrends.com/get-last-month-downloads-badge/pyserde.svg"></a>
 </p>
 <p align="center">
   <a href="https://yukinarit.github.io/pyserde/guide/en">Guide🇬🇧</a> | <a href="https://yukinarit.github.io/pyserde/guide/ja">ガイド🇯🇵</a> | <a href="https://yukinarit.github.io/pyserde/api/serde.html">API Reference</a> | <a href="https://github.com/yukinarit/pyserde/tree/main/examples">Examples</a>
diff --git a/pyproject.toml b/pyproject.toml
index 2fca645..b7695eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,7 +54,7 @@ numpy = [
     { version = ">1.22.0,<2.0.0", markers = "python_version ~= '3.11'" },
     { version = ">1.22.0,<2.0.0", markers = "python_version ~= '3.12'" },
 ]
-mypy = "==1.10.1"
+mypy = "==1.10.0"
 pytest = "*"
 pytest-cov = "*"
 pytest-watch = "*"
@@ -153,6 +153,7 @@ select = [
     "C",  # flake8-comprehensions
     "B",  # flake8-bugbear
 ]
+ignore = ["B904"]
 line-length = 100
 
 [tool.ruff.lint.mccabe]
diff --git a/serde/core.py b/serde/core.py
index aed1301..ad1f17c 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -937,8 +937,13 @@ coerce = TypeCheck(kind=TypeCheck.Kind.Coerce)
 strict = TypeCheck(kind=TypeCheck.Kind.Strict)
 
 
-def coerce_object(typ: type[Any], obj: Any) -> Any:
-    return typ(obj) if is_coercible(typ, obj) else obj
+def coerce_object(cls: str, field: str, typ: type[Any], obj: Any) -> Any:
+    try:
+        return typ(obj) if is_coercible(typ, obj) else obj
+    except Exception as e:
+        raise SerdeError(
+            f"failed to coerce the field {cls}.{field} value {obj} into {typename(typ)}: {e}"
+        )
 
 
 def is_coercible(typ: type[Any], obj: Any) -> bool:
diff --git a/serde/de.py b/serde/de.py
index 6356bfd..047be4d 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -701,6 +701,7 @@ class Renderer:
     suppress_coerce: bool = False
     """ Disable type coercing in codegen """
     class_deserializer: Optional[ClassDeserializer] = None
+    class_name: Optional[str] = None
 
     def render(self, arg: DeField[Any]) -> str:
         """
@@ -844,23 +845,6 @@ class Renderer:
     def opt(self, arg: DeField[Any]) -> str:
         """
         Render rvalue for Optional.
-
-        >>> Renderer('foo').render(DeField(Optional[int], 'o', datavar='data'))
-        '(coerce_object(int, data["o"])) if data.get("o") is not None else None'
-
-        >>> Renderer('foo').render(DeField(Optional[list[int]], 'o', datavar='data'))
-        '([coerce_object(int, v) for v in data["o"]]) if data.get("o") is not None else None'
-
-        >>> Renderer('foo').render(DeField(Optional[list[int]], 'o', datavar='data'))
-        '([coerce_object(int, v) for v in data["o"]]) if data.get("o") is not None else None'
-
-        >>> @deserialize
-        ... class Foo:
-        ...     o: Optional[list[int]]
-        >>> Renderer('foo').render(DeField(Optional[Foo], 'f', datavar='data'))
-        '(Foo.__serde__.funcs[\\'foo\\'](data=data["f"], maybe_generic=maybe_generic, \
-maybe_generic_type_vars=maybe_generic_type_vars, variable_type_args=None, \
-reuse_instances=reuse_instances)) if data.get("f") is not None else None'
         """
         inner = arg[0]
         if arg.iterbased:
@@ -886,12 +870,6 @@ reuse_instances=reuse_instances)) if data.get("f") is not None else None'
     def list(self, arg: DeField[Any]) -> str:
         """
         Render rvalue for list.
-
-        >>> Renderer('foo').render(DeField(list[int], 'l', datavar='data'))
-        '[coerce_object(int, v) for v in data["l"]]'
-
-        >>> Renderer('foo').render(DeField(list[list[int]], 'l', datavar='data'))
-        '[[coerce_object(int, v) for v in v] for v in data["l"]]'
         """
         if is_bare_list(arg.type):
             return f"list({arg.data})"
@@ -901,13 +879,6 @@ reuse_instances=reuse_instances)) if data.get("f") is not None else None'
     def set(self, arg: DeField[Any]) -> str:
         """
         Render rvalue for set.
-
-        >>> from typing import Set
-        >>> Renderer('foo').render(DeField(Set[int], 'l', datavar='data'))
-        'set(coerce_object(int, v) for v in data["l"])'
-
-        >>> Renderer('foo').render(DeField(Set[Set[int]], 'l', datavar='data'))
-        'set(set(coerce_object(int, v) for v in v) for v in data["l"])'
         """
         if is_bare_set(arg.type):
             return f"set({arg.data})"
@@ -919,26 +890,6 @@ reuse_instances=reuse_instances)) if data.get("f") is not None else None'
     def tuple(self, arg: DeField[Any]) -> str:
         """
         Render rvalue for tuple.
-
-        >>> @deserialize
-        ... class Foo: pass
-        >>> Renderer('foo').render(DeField(tuple[str, int, list[int], Foo], 'd', datavar='data'))
-        '(coerce_object(str, data["d"][0]), coerce_object(int, data["d"][1]), \
-[coerce_object(int, v) for v in data["d"][2]], \
-Foo.__serde__.funcs[\\'foo\\'](data=data["d"][3], maybe_generic=maybe_generic, \
-maybe_generic_type_vars=maybe_generic_type_vars, variable_type_args=None, \
-reuse_instances=reuse_instances),)'
-
-        >>> field = DeField(tuple[str, int, list[int], Foo],
-        ...                'd',
-        ...                 datavar='data',
-        ...                 index=0,
-        ...                 iterbased=True)
-        >>> Renderer('foo').render(field)
-        "(coerce_object(str, data[0][0]), coerce_object(int, data[0][1]), \
-[coerce_object(int, v) for v in data[0][2]], Foo.__serde__.funcs['foo'](data=data[0][3], \
-maybe_generic=maybe_generic, maybe_generic_type_vars=maybe_generic_type_vars, \
-variable_type_args=None, reuse_instances=reuse_instances),)"
         """
         if is_bare_tuple(arg.type):
             return f"tuple({arg.data})"
@@ -956,21 +907,6 @@ variable_type_args=None, reuse_instances=reuse_instances),)"
     def dict(self, arg: DeField[Any]) -> str:
         """
         Render rvalue for dict.
-
-        >>> Renderer('foo').render(DeField(dict[str, int], 'd', datavar='data'))
-        '{coerce_object(str, k): coerce_object(int, v) for k, v in data["d"].items()}'
-
-        >>> @deserialize
-        ... class Foo: pass
-        >>> Renderer('foo').render(DeField(dict[Foo, list[Foo]], 'f', datavar='data'))
-        '\
-{Foo.__serde__.funcs[\\'foo\\'](data=k, maybe_generic=maybe_generic, \
-maybe_generic_type_vars=maybe_generic_type_vars, variable_type_args=None, \
-reuse_instances=reuse_instances): \
-[Foo.__serde__.funcs[\\'foo\\'](data=v, maybe_generic=maybe_generic, \
-maybe_generic_type_vars=maybe_generic_type_vars, \
-variable_type_args=None, reuse_instances=reuse_instances) for v in v] \
-for k, v in data["f"].items()}'
         """
         if is_bare_dict(arg.type):
             return arg.data
@@ -1000,15 +936,6 @@ for k, v in data["f"].items()}'
         Render rvalue for primitives.
 
         * `suppress_coerce`: Overrides "suppress_coerce" in the Renderer's field
-
-        >>> Renderer('foo').render(DeField(int, 'i', datavar='data'))
-        'coerce_object(int, data["i"])'
-
-        >>> Renderer('foo').render(DeField(int, 'int_field', datavar='data', case='camelcase'))
-        'coerce_object(int, data["intField"])'
-
-        >>> Renderer('foo').render(DeField(int, 'i', datavar='data', index=1, iterbased=True))
-        'coerce_object(int, data[1])'
         """
         typ = typename(arg.type)
         dat = arg.data
@@ -1018,7 +945,7 @@ for k, v in data["f"].items()}'
         if self.suppress_coerce and suppress_coerce:
             return dat
         else:
-            return f"coerce_object({typ}, {dat})"
+            return f'coerce_object("{self.class_name}", "{arg.name}", {typ}, {dat})'
 
     def c_tor(self, arg: DeField[Any]) -> str:
         return f"{typename(arg.type)}({arg.data})"
@@ -1193,6 +1120,7 @@ def render_from_iter(
         legacy_class_deserializer=legacy_class_deserializer,
         suppress_coerce=(not type_check.is_coerce()),
         class_deserializer=class_deserializer,
+        class_name=typename(cls),
     )
     fields = list(filter(renderable, defields(cls)))
     res = jinja2_env.get_template("iter").render(
@@ -1223,6 +1151,7 @@ def render_from_dict(
         legacy_class_deserializer=legacy_class_deserializer,
         suppress_coerce=(not type_check.is_coerce()),
         class_deserializer=class_deserializer,
+        class_name=typename(cls),
     )
     fields = list(filter(renderable, defields(cls)))
     res = jinja2_env.get_template("dict").render(
diff --git a/serde/se.py b/serde/se.py
index 52e8702..1501a51 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -610,6 +610,7 @@ def render_to_tuple(
         suppress_coerce=(not type_check.is_coerce()),
         serialize_class_var=serialize_class_var,
         class_serializer=class_serializer,
+        class_name=typename(cls),
     )
     return jinja2_env.get_template("iter").render(
         func=TO_ITER,
@@ -633,6 +634,7 @@ def render_to_dict(
         legacy_class_serializer,
         suppress_coerce=(not type_check.is_coerce()),
         class_serializer=class_serializer,
+        class_name=typename(cls),
     )
     lrenderer = LRenderer(case, serialize_class_var)
     return jinja2_env.get_template("dict").render(
@@ -652,7 +654,7 @@ def render_union_func(
     Render function that serializes a field with union type.
     """
     union_name = f"Union[{', '.join([typename(a) for a in union_args])}]"
-    renderer = Renderer(TO_DICT, suppress_coerce=True)
+    renderer = Renderer(TO_DICT, suppress_coerce=True, class_name=typename(cls))
     return jinja2_env.get_template("union").render(
         func=union_func_name(UNION_SE_PREFIX, union_args),
         serde_scope=getattr(cls, SERDE_SCOPE),
@@ -710,46 +712,11 @@ class Renderer:
     """ Suppress type coercing because generated union serializer has its own type checking """
     serialize_class_var: bool = False
     class_serializer: Optional[ClassSerializer] = None
+    class_name: Optional[str] = None
 
     def render(self, arg: SeField[Any]) -> str:
         """
         Render rvalue
-
-        >>> Renderer(TO_ITER).render(SeField(int, 'i'))
-        'coerce_object(int, i)'
-
-        >>> Renderer(TO_ITER).render(SeField(list[int], 'l'))
-        '[coerce_object(int, v) for v in l]'
-
-        >>> @serialize
-        ... @dataclass(unsafe_hash=True)
-        ... class Foo:
-        ...    val: int
-        >>> Renderer(TO_ITER).render(SeField(Foo, 'foo'))
-        "\
-foo.__serde__.funcs['to_iter'](foo, reuse_instances=reuse_instances, convert_sets=convert_sets)"
-
-        >>> Renderer(TO_ITER).render(SeField(list[Foo], 'foo'))
-        "\
-[v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \
-convert_sets=convert_sets) for v in foo]"
-
-        >>> Renderer(TO_ITER).render(SeField(dict[str, Foo], 'foo'))
-        "\
-{coerce_object(str, k): v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \
-convert_sets=convert_sets) for k, v in foo.items()}"
-
-        >>> Renderer(TO_ITER).render(SeField(dict[Foo, Foo], 'foo'))
-        "\
-{k.__serde__.funcs['to_iter'](k, reuse_instances=reuse_instances, \
-convert_sets=convert_sets): v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \
-convert_sets=convert_sets) for k, v in foo.items()}"
-
-        >>> Renderer(TO_ITER).render(SeField(tuple[str, Foo, int], 'foo'))
-        "\
-(coerce_object(str, foo[0]), foo[1].__serde__.funcs['to_iter'](foo[1], \
-reuse_instances=reuse_instances, convert_sets=convert_sets), \
-coerce_object(int, foo[2]),)"
         """
         implemented_methods: dict[type[Any], int] = {}
         class_serializers: Iterable[ClassSerializer] = itertools.chain(
@@ -925,7 +892,7 @@ coerce_object(int, foo[2]),)"
         if self.suppress_coerce:
             return var
         else:
-            return f"coerce_object({typ}, {var})"
+            return f'coerce_object("{self.class_name}", "{arg.name}", {typ}, {var})'
 
     def string(self, arg: SeField[Any]) -> str:
         return f"str({arg.varname})"


Test patch:
diff --git a/tests/test_de.py b/tests/test_de.py
index 26290b4..d6e2d10 100644
--- a/tests/test_de.py
+++ b/tests/test_de.py
@@ -1,6 +1,6 @@
 from decimal import Decimal
-from typing import Union
-from serde.de import from_obj
+from typing import Union, Optional
+from serde.de import deserialize, from_obj, Renderer, DeField
 
 
 def test_from_obj() -> None:
@@ -23,3 +23,105 @@ def test_from_obj() -> None:
     dec = from_obj(list[Decimal], ("0.1", 0.1), False, False)
     assert isinstance(dec[0], Decimal) and dec[0] == Decimal("0.1")
     assert isinstance(dec[1], Decimal) and dec[1] == Decimal(0.1)
+
+
+kwargs = (
+    "maybe_generic=maybe_generic, maybe_generic_type_vars=maybe_generic_type_vars, "
+    + "variable_type_args=None, reuse_instances=reuse_instances"
+)
+
+
+def test_render_primitives() -> None:
+
+    rendered = Renderer("foo").render(DeField(int, "i", datavar="data"))
+    assert rendered == 'coerce_object("None", "i", int, data["i"])'
+
+    rendered = Renderer("foo").render(DeField(int, "int_field", datavar="data", case="camelcase"))
+    assert rendered == 'coerce_object("None", "int_field", int, data["intField"])'
+
+    rendered = Renderer("foo").render(DeField(int, "i", datavar="data", index=1, iterbased=True))
+    assert rendered == 'coerce_object("None", "i", int, data[1])'
+
+
+def test_render_list() -> None:
+    rendered = Renderer("foo").render(DeField(list[int], "l", datavar="data"))
+    assert rendered == '[coerce_object("None", "v", int, v) for v in data["l"]]'
+
+    rendered = Renderer("foo").render(DeField(list[list[int]], "l", datavar="data"))
+    assert rendered == '[[coerce_object("None", "v", int, v) for v in v] for v in data["l"]]'
+
+
+def test_render_tuple() -> None:
+    @deserialize
+    class Foo:
+        pass
+
+    rendered = Renderer("foo").render(DeField(tuple[str, int, list[int], Foo], "d", datavar="data"))
+    rendered_str = 'coerce_object("None", "data["d"][0]", str, data["d"][0])'
+    rendered_int = 'coerce_object("None", "data["d"][1]", int, data["d"][1])'
+    rendered_lst = '[coerce_object("None", "v", int, v) for v in data["d"][2]]'
+    rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[\"d\"][3], {kwargs})"
+    assert rendered == f"({rendered_str}, {rendered_int}, {rendered_lst}, {rendered_foo},)"
+
+    field = DeField(tuple[str, int, list[int], Foo], "d", datavar="data", index=0, iterbased=True)
+    rendered = Renderer("foo").render(field)
+    rendered_str = 'coerce_object("None", "data[0][0]", str, data[0][0])'
+    rendered_int = 'coerce_object("None", "data[0][1]", int, data[0][1])'
+    rendered_lst = '[coerce_object("None", "v", int, v) for v in data[0][2]]'
+    rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[0][3], {kwargs})"
+    assert rendered == f"({rendered_str}, {rendered_int}, {rendered_lst}, {rendered_foo},)"
+
+
+def test_render_dict() -> None:
+    rendered = Renderer("foo").render(DeField(dict[str, int], "d", datavar="data"))
+    rendered_key = 'coerce_object("None", "k", str, k)'
+    rendered_val = 'coerce_object("None", "v", int, v)'
+    rendered_dct = f'{{{rendered_key}: {rendered_val} for k, v in data["d"].items()}}'
+    assert rendered == rendered_dct
+
+    @deserialize
+    class Foo:
+        pass
+
+    rendered = Renderer("foo").render(DeField(dict[Foo, list[Foo]], "f", datavar="data"))
+    rendered_key = f"Foo.__serde__.funcs['foo'](data=k, {kwargs})"
+    rendered_val = f"[Foo.__serde__.funcs['foo'](data=v, {kwargs}) for v in v]"
+
+    assert rendered == f'{{{rendered_key}: {rendered_val} for k, v in data["f"].items()}}'
+
+
+def test_render_set() -> None:
+    from typing import Set
+
+    rendered = Renderer("foo").render(DeField(Set[int], "l", datavar="data"))
+    assert rendered == 'set(coerce_object("None", "v", int, v) for v in data["l"])'
+
+    rendered = Renderer("foo").render(DeField(Set[Set[int]], "l", datavar="data"))
+    assert rendered == 'set(set(coerce_object("None", "v", int, v) for v in v) for v in data["l"])'
+
+
+def test_render_opt() -> None:
+    rendered = Renderer("foo").render(DeField(Optional[int], "o", datavar="data"))  # type: ignore
+    rendered_opt = (
+        '(coerce_object("None", "o", int, data["o"])) if data.get("o") is not None else None'
+    )
+    assert rendered == rendered_opt
+
+    rendered = Renderer("foo").render(DeField(Optional[list[int]], "o", datavar="data"))  # type: ignore
+    rendered_lst = '[coerce_object("None", "v", int, v) for v in data["o"]]'
+    rendered_opt = f'({rendered_lst}) if data.get("o") is not None else None'
+    assert rendered == rendered_opt
+
+    rendered = Renderer("foo").render(DeField(Optional[list[int]], "o", datavar="data"))  # type: ignore
+    rendered_lst = '[coerce_object("None", "v", int, v) for v in data["o"]]'
+    rendered_opt = f'({rendered_lst}) if data.get("o") is not None else None'
+    assert rendered == rendered_opt
+
+    @deserialize
+    class Foo:
+        a: Optional[list[int]]
+
+    rendered = Renderer("foo").render(DeField(Optional[Foo], "f", datavar="data"))  # type: ignore
+    rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[\"f\"], {kwargs})"
+    rendered_opt = f'({rendered_foo}) if data.get("f") is not None else None'
+    assert rendered == rendered_opt
diff --git a/tests/test_se.py b/tests/test_se.py
index 2697c5f..a82e379 100644
--- a/tests/test_se.py
+++ b/tests/test_se.py
@@ -1,6 +1,8 @@
 from serde import asdict, astuple, serialize, to_dict, to_tuple
 from serde.json import to_json
 from serde.msgpack import to_msgpack
+from serde.se import SeField, Renderer
+from serde.core import TO_ITER
 
 from . import data
 from .data import (
@@ -128,3 +130,51 @@ def test_convert_sets_option() -> None:
     assert a_dict == {"v": ["a", "b"]} or a_dict == {"v": ["b", "a"]}
 
     assert {"v": {"a", "b"}} == to_dict(a, convert_sets=False)
+
+
+@serialize
+class Foo:
+    val: int
+
+
+kwargs = "reuse_instances=reuse_instances, convert_sets=convert_sets"
+
+
+def test_render_primitives() -> None:
+    rendered = Renderer(TO_ITER).render(SeField(int, "i"))
+    assert rendered == 'coerce_object("None", "i", int, i)'
+
+
+def test_render_list() -> None:
+
+    rendered = Renderer(TO_ITER).render(SeField(list[int], "l"))
+    assert rendered == '[coerce_object("None", "v", int, v) for v in l]'
+
+    rendered = Renderer(TO_ITER).render(SeField(list[Foo], "foo"))
+    assert rendered == f"[v.__serde__.funcs['to_iter'](v, {kwargs}) for v in foo]"
+
+
+def test_render_dict() -> None:
+    rendered = Renderer(TO_ITER).render(SeField(dict[str, Foo], "foo"))
+    rendered_key = 'coerce_object("None", "k", str, k)'
+    rendered_val = f"v.__serde__.funcs['to_iter'](v, {kwargs})"
+    assert rendered == f"{{{rendered_key}: {rendered_val} for k, v in foo.items()}}"
+
+    rendered = Renderer(TO_ITER).render(SeField(dict[Foo, Foo], "foo"))
+    rendered_key = f"k.__serde__.funcs['to_iter'](k, {kwargs})"
+    rendered_val = f"v.__serde__.funcs['to_iter'](v, {kwargs})"
+    assert rendered == f"{{{rendered_key}: {rendered_val} for k, v in foo.items()}}"
+
+
+def test_render_tuple() -> None:
+    rendered = Renderer(TO_ITER).render(SeField(tuple[str, Foo, int], "foo"))
+    rendered_str = 'coerce_object("None", "foo[0]", str, foo[0])'
+    rendered_foo = f"foo[1].__serde__.funcs['to_iter'](foo[1], {kwargs})"
+    rendered_int = 'coerce_object("None", "foo[2]", int, foo[2])'
+    assert rendered == f"({rendered_str}, {rendered_foo}, {rendered_int},)"
+
+
+def test_render_dataclass() -> None:
+    rendered = Renderer(TO_ITER).render(SeField(Foo, "foo"))
+    rendered_foo = f"foo.__serde__.funcs['to_iter'](foo, {kwargs})"
+    assert rendered_foo == rendered