Repository: yukinarit/pyserde

Issue:
Bear type checking "leaks" to classes without the @serde decorator
Hi,

With update 0.14, it seems that bear type checking is being applied to classes passed as arguments to `serde.json.to_json`, even if they do not have the `@serde` decorator applied to them.

In general I prefer to not use the `@serde` decorator in my model classes, instead using `serde` only at the I/O layer with the `serde.json.from_json` and `serde.json.to_json` functions.

However seems like after calling `to_json` into a dataclass without the `@serde` decorator, bear type checking is now applied to that class from that point onward, which is surprising.

Example:

```python
from dataclasses import dataclass

import serde.json


@dataclass
class Foo:
    value: int


# This passes.
f = Foo("100")

data = serde.json.to_json(f, cls=Foo)

# After to_json(), this starts to fail with:
# beartype.roar.BeartypeCallHintParamViolation: Method __main__.Foo.__init__() parameter value='100' violates type hint <class 'int'>, as str '100' not instance of int.
f = Foo("100")
```

This is surprising to the user, even more so because model classes start to validate their types *after* some I/O has been performed, which might happen at different points in the application.

A workaround for now is to decorate `Foo` with `@serde(type_check=disabled)`, however this is not ideal given the functions `to_json` and `from_json` do not convey that they might change the runtime behavior of the class being passed as parameter.


> [!Note]
> This example is simplified, but in my code it was more complex, where I had an attribute declared as `tuple[int, ...]` but would accept any `Sequence[int]` at runtime, being converted to `tuple[int, ...]` during `__post_init__` to ensure the constructed object would have the correct runtime type. The reason for this is that the `Sequence[int]` comes from an external source, and sometimes would be provided as `list[int]` or `tuple[int, ...]`.

Discussion:

Patch:
diff --git a/serde/de.py b/serde/de.py
index 5451cf6..5674f0a 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -438,7 +438,10 @@ def from_obj(c: Type[T], o: Any, named: bool, reuse_instances: Optional[bool]) -
     try:
         thisfunc = functools.partial(from_obj, named=named, reuse_instances=reuse_instances)
         if is_dataclass_without_de(c):
-            deserialize(c)
+            # Do not automatically implement beartype if dataclass without serde decorator
+            # is passed, because it is surprising for users
+            # See https://github.com/yukinarit/pyserde/issues/480
+            deserialize(c, type_check=disabled)
             res = deserializable_to_obj(c)
         elif is_deserializable(c):
             res = deserializable_to_obj(c)
diff --git a/serde/se.py b/serde/se.py
index eca1dcc..a69544f 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -366,7 +366,10 @@ def to_obj(
         if o is None:
             return None
         if is_dataclass_without_se(o):
-            serialize(type(o))
+            # Do not automatically implement beartype if dataclass without serde decorator
+            # is passed, because it is surprising for users
+            # See https://github.com/yukinarit/pyserde/issues/480
+            serialize(type(o), type_check=disabled)
             return serializable_to_obj(o)
         elif is_serializable(o):
             return serializable_to_obj(o)


Test patch:
diff --git a/tests/test_type_check.py b/tests/test_type_check.py
index 86caad6..9f3dd63 100644
--- a/tests/test_type_check.py
+++ b/tests/test_type_check.py
@@ -1,3 +1,4 @@
+from dataclasses import dataclass
 import datetime
 import pathlib
 from beartype.roar import BeartypeCallHintViolation
@@ -15,6 +16,7 @@ from beartype.typing import (
 import pytest
 
 import serde
+import serde.json
 
 from . import data
 
@@ -100,6 +102,18 @@ def test_type_check_strict(T: Any, data: Any, exc: bool) -> None:
         serde.from_dict(C, d)
 
 
+def test_type_check_disabled_for_dataclass_without_serde() -> None:
+    @dataclass
+    class Foo:
+        value: int
+
+    f = Foo("100")  # type: ignore
+    data = serde.json.to_json(f)
+    assert f == serde.json.from_json(Foo, data)
+
+    f = Foo("100")  # type: ignore
+
+
 def test_uncoercible() -> None:
     @serde.serde(type_check=serde.coerce)
     class Foo: