Repository: yukinarit/pyserde

Issue:
Error when parsing Union types with different ordering
Tested versions:
 - Python: **3.10.4**
 - Pyserde: **0.12.6**


If you define two unions with the same members, but different orders, then attempting to parse a document throws an error.

Example:
```python
from serde import serde
from serde.json import from_json

@serde
class Expr:
    expr: str


MaybeExpr = str | Expr
MaybeExpr2 = Expr | str

@serde
class Test:
    a: MaybeExpr
    b: MaybeExpr2

if __name__ == '__main__':
    print(from_json(Test, '{"a": "x", "b": "y"}'))

```

Result:
```
Traceback (most recent call last):
  File "/Users/sstagg/tmp/pystest/test.py", line 18, in <module>
    print(from_json(Test, '{"a": "x", "b": "y"}'))
  File "/Users/sstagg/.pyenv/versions/3.10.4/lib/python3.10/site-packages/serde/json.py", line 95, in from_json
    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False)
  File "/Users/sstagg/.pyenv/versions/3.10.4/lib/python3.10/site-packages/serde/de.py", line 494, in from_dict
    return from_obj(cls, o, named=True, reuse_instances=reuse_instances)
  File "/Users/sstagg/.pyenv/versions/3.10.4/lib/python3.10/site-packages/serde/de.py", line 460, in from_obj
    raise SerdeError(e) from None
serde.compat.SerdeError: 'union_de_Expr_str'
```

Arguably this is a something trivially fixed by not having multiple Unions with different types, but the error message is a bit tricky to debug here, and is very possible with more complex scenarios.

It looks a bit like member order is ignored when constructing the ser/de methods (makes sense), but is relied on when looking up the ser/de method.

I guess it might make sense to sort the member names in the method name for a union?


Discussion:

Patch:
diff --git a/serde/compat.py b/serde/compat.py
index 5a78a47..2b3ccb9 100644
--- a/serde/compat.py
+++ b/serde/compat.py
@@ -355,17 +355,15 @@ def iter_unions(cls: TypeLike) -> list[TypeLike]:
     """
     Iterate over all unions that are used in the dataclass
     """
-    lst: set[TypeLike] = set()
+    lst: list[TypeLike] = []
     stack: list[TypeLike] = []  # To prevent infinite recursion
 
     def recursive(cls: TypeLike) -> None:
-        if cls in lst:
-            return
         if cls in stack:
             return
 
         if is_union(cls):
-            lst.add(cls)
+            lst.append(cls)
             for arg in type_args(cls):
                 recursive(arg)
         if is_dataclass(cls):
@@ -391,7 +389,7 @@ def iter_unions(cls: TypeLike) -> list[TypeLike]:
                 recursive(args[1])
 
     recursive(cls)
-    return list(lst)
+    return lst
 
 
 def iter_literals(cls: type[Any]) -> list[TypeLike]:


Test patch:
diff --git a/tests/test_union.py b/tests/test_union.py
index 8021446..75fa80d 100644
--- a/tests/test_union.py
+++ b/tests/test_union.py
@@ -804,3 +804,21 @@ def test_union_with_any() -> None:
 
     c = Class([FooWithString("string"), BarWithDict({"key": "value"})])
     assert c == from_json(Class, to_json(c))
+
+
+def test_union_with_different_ordering() -> None:
+
+    @serde
+    class Expr:
+        expr: str
+
+    @serde
+    class Foo:
+        a: Union[str, Expr]
+        b: Union[Expr, str]
+        c: Union[Expr, str]
+
+    f = Foo("1", Expr("2"), "3")
+    s = '{"a":"1","b":{"Expr":{"expr":"2"}},"c":"3"}'
+    assert s == to_json(f)
+    assert f == from_json(Foo, s)