Repository: yukinarit/pyserde

Issue:
Python 3.10 union operator with None (e.g. " str | None") can't capture missing field 
I'm running Python 3.10 and Pyserde 0.7.2. This
```python
@serde
class Test:
    x: str | None

print(from_json(Test, '{}'))
```
causes a crash
```
Traceback (most recent call last):
  File "/home/seho/.local/lib/python3.10/site-packages/serde/de.py", line 304, in from_obj
    return serde_scope.funcs[FROM_DICT](
  File "<string>", line 11, in from_dict
KeyError: 'x'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/seho/pts15_test/deleteme.py", line 11, in <module>
    print(from_json(Test, '{}'))
  File "/home/seho/.local/lib/python3.10/site-packages/serde/json.py", line 47, in from_json
    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False)
  File "/home/seho/.local/lib/python3.10/site-packages/serde/de.py", line 376, in from_dict
    return from_obj(cls, o, named=True, reuse_instances=reuse_instances)
  File "/home/seho/.local/lib/python3.10/site-packages/serde/de.py", line 353, in from_obj
    raise SerdeError(e)
serde.compat.SerdeError: 'x'
```
While using `Optional[str]` works fine
```python
@serde
class Test:
    x: Optional[str]

print(from_json(Test, '{}'))
```
Output: `test(x=None)`

Comments:

Patch:
diff --git a/examples/union_operator.py b/examples/union_operator.py
index a6d2f68..cb78577 100644
--- a/examples/union_operator.py
+++ b/examples/union_operator.py
@@ -17,6 +17,7 @@ class Foo:
     a: int | str
     b: dict[str, int] | list[int]
     c: Bar | Baz
+    d: str | None = None  # Should be treated as Optional
 
 
 def main():
diff --git a/serde/compat.py b/serde/compat.py
index b5e2143..dc8b5a5 100644
--- a/serde/compat.py
+++ b/serde/compat.py
@@ -354,16 +354,16 @@ def is_union(typ) -> bool:
     True
     """
 
-    is_union_type = False
+    # Python 3.10 Union operator e.g. str | int
     if sys.version_info[:2] >= (3, 10):
         try:
-            is_union_type = isinstance(typ, types.UnionType) and not is_opt(typ)
+            if isinstance(typ, types.UnionType):
+                return True
         except Exception:
             pass
 
-    typing_union = typing_inspect.is_union_type(typ) and not is_opt(typ)
-
-    return is_union_type or typing_union
+    # typing.Union
+    return typing_inspect.is_union_type(typ)
 
 
 def is_opt(typ) -> bool:
@@ -377,9 +377,22 @@ def is_opt(typ) -> bool:
     >>> is_opt(None.__class__)
     False
     """
+
+    # Python 3.10 Union operator e.g. str | None
+    is_union_type = False
+    if sys.version_info[:2] >= (3, 10):
+        try:
+            if isinstance(typ, types.UnionType):
+                is_union_type = True
+        except Exception:
+            pass
+
+    # typing.Optional
+    is_typing_union = typing_inspect.is_optional_type(typ)
+
     args = type_args(typ)
     if args:
-        return typing_inspect.is_optional_type(typ) and len(args) == 2 and not is_none(args[0]) and is_none(args[1])
+        return (is_union_type or is_typing_union) and len(args) == 2 and not is_none(args[0]) and is_none(args[1])
     else:
         return typ is Optional
 


Test patch:
diff --git a/tests/test_compat.py b/tests/test_compat.py
index 8991865..2f6ba0b 100644
--- a/tests/test_compat.py
+++ b/tests/test_compat.py
@@ -37,10 +37,10 @@ def test_types():
     assert is_dict(Dict[str, int])
     assert is_dict(Dict)
     assert is_opt(Optional[int])
+    assert is_opt(Union[int, None])
     assert is_union(Union[int, str])
     assert is_union(Union[Optional[int], Optional[str]])
-    assert is_opt(Optional[int])
-    assert not is_union(Optional[int])
+    assert is_union(Optional[int])
     assert not is_opt(Union[Optional[int], Optional[str]])
     assert is_union(Union[Optional[int], Optional[str]])
 
@@ -58,6 +58,11 @@ def test_types():
         assert is_tuple(tuple[int, int, int])
         assert is_dict(dict[str, int])
 
+    if sys.version_info[:3] >= (3, 10, 0):
+        assert is_union(str | int)
+        assert is_union(str | None)
+        assert is_opt(str | None)
+
 
 def test_typename():
     @serde.serde