Repository: Fatal1ty/mashumaro

Issue:
Add support for ForwardRef
* mashumaro version: 3.9.1
* Python version: 3.10.12
* Operating System: Linux

### Description

I'm trying to use mashumaro to deserialize a `TypedDict`, got an exception like:

```python
Traceback (most recent call last):
  File "/home/ubuntu/workspace/example/python/repro.py", line 18, in <module>
    class ConfigChecker(DataClassDictMixin):
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/mixins/dict.py", line 24, in __init_subclass__
    compile_mixin_unpacker(cls, **builder_params["unpacker"])
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/mixin.py", line 49, in compile_mixin_unpacker
    builder.add_unpack_method()
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 474, in add_unpack_method
    self._add_unpack_method_lines(method_name)
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 400, in _add_unpack_method_lines
    self._unpack_method_set_value(
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 514, in _unpack_method_set_value
    unpacked_value = UnpackerRegistry.get(
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/types/common.py", line 119, in get
    expr = packer(spec)
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/types/unpack.py", line 1098, in unpack_collection
    return unpack_typed_dict(spec)
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/types/unpack.py", line 983, in unpack_typed_dict
    unpacker = UnpackerRegistry.get(
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/types/common.py", line 119, in get
    expr = packer(spec)
  File "/home/ubuntu/workspace/example/.venv/lib/python3.10/site-packages/mashumaro/core/meta/types/unpack.py", line 706, in unpack_special_typing_primitive
    raise UnserializableDataError(
mashumaro.exceptions.UnserializableDataError: ForwardRef('PreCheckConfig') as a field type is not supported by mashumaro
```

Is this unsupported at the moment or cannot be supported? Thanks!

### What I Did

```python
from __future__ import annotations

from dataclasses import dataclass
from typing import TypedDict

from mashumaro.mixins.dict import DataClassDictMixin


class PreCheckConfig(TypedDict):
    enable: bool


class Config(TypedDict):
    pre_check: PreCheckConfig


@dataclass
class ConfigChecker(DataClassDictMixin):
    config: Config


ConfigChecker.from_dict({"config": {"pre_check": {"enable": True}}})

```


Discussion:

Patch:
diff --git a/mashumaro/core/meta/code/builder.py b/mashumaro/core/meta/code/builder.py
index 00c16db..2c353a2 100644
--- a/mashumaro/core/meta/code/builder.py
+++ b/mashumaro/core/meta/code/builder.py
@@ -1,5 +1,6 @@
 import importlib
 import inspect
+import sys
 import types
 import typing
 from contextlib import contextmanager
@@ -28,6 +29,7 @@ from mashumaro.core.const import Sentinel
 from mashumaro.core.helpers import ConfigValue
 from mashumaro.core.meta.code.lines import CodeLines
 from mashumaro.core.meta.helpers import (
+    evaluate_forward_ref,
     get_args,
     get_class_that_defines_field,
     get_class_that_defines_method,
@@ -277,6 +279,25 @@ class CodeBuilder:
             print(code)
         exec(code, self.globals, self.__dict__)
 
+    def evaluate_forward_ref(
+        self,
+        typ: typing.ForwardRef,
+        owner: typing.Optional[typing.Type],
+    ) -> typing.Optional[typing.Type]:
+        if not getattr(typ, "__forward_module__", None) and owner:
+            # We can't get the module in which ForwardRef is defined on
+            # Python < 3.10, ForwardRef evaluation might not work properly
+            # without this information, so we will consider the namespace of
+            # the module in which this ForwardRef is used as globalns.
+            globalns = getattr(
+                sys.modules.get(owner.__module__, None),
+                "__dict__",
+                self.globals,
+            )
+        else:
+            globalns = self.globals
+        return evaluate_forward_ref(typ, globalns, self.__dict__)
+
     def get_declared_hook(self, method_name: str) -> typing.Any:
         cls = get_class_that_defines_method(method_name, self.cls)
         if cls is not None and not is_dataclass_dict_mixin(cls):
diff --git a/mashumaro/core/meta/helpers.py b/mashumaro/core/meta/helpers.py
index fdf7945..cb22df7 100644
--- a/mashumaro/core/meta/helpers.py
+++ b/mashumaro/core/meta/helpers.py
@@ -13,6 +13,7 @@ from typing import (
     Any,
     ClassVar,
     Dict,
+    ForwardRef,
     List,
     Optional,
     Sequence,
@@ -79,6 +80,7 @@ __all__ = [
     "hash_type_args",
     "iter_all_subclasses",
     "is_hashable",
+    "evaluate_forward_ref",
 ]
 
 
@@ -727,3 +729,20 @@ def is_hashable(typ: Any) -> bool:
         return True
     except TypeError:
         return False
+
+
+if PY_39_MIN:
+
+    def evaluate_forward_ref(
+        typ: ForwardRef, globalns: Any, localns: Any
+    ) -> Optional[Type]:
+        return typ._evaluate(
+            globalns, localns, frozenset()
+        )  # type: ignore[call-arg]
+
+else:
+
+    def evaluate_forward_ref(
+        typ: ForwardRef, globalns: Any, localns: Any
+    ) -> Optional[Type]:
+        return typ._evaluate(globalns, localns)  # type: ignore[call-arg]
diff --git a/mashumaro/core/meta/types/common.py b/mashumaro/core/meta/types/common.py
index 9476f53..3e33e38 100644
--- a/mashumaro/core/meta/types/common.py
+++ b/mashumaro/core/meta/types/common.py
@@ -77,6 +77,7 @@ class ValueSpec:
     field_ctx: FieldContext
     could_be_none: bool = True
     annotated_type: Optional[Type] = None
+    owner: Optional[Type] = None
 
     def __setattr__(self, key: str, value: Any) -> None:
         if key == "type":
diff --git a/mashumaro/core/meta/types/pack.py b/mashumaro/core/meta/types/pack.py
index f6b106a..475b1aa 100644
--- a/mashumaro/core/meta/types/pack.py
+++ b/mashumaro/core/meta/types/pack.py
@@ -9,7 +9,16 @@ from contextlib import suppress
 from dataclasses import is_dataclass
 from decimal import Decimal
 from fractions import Fraction
-from typing import Any, Callable, List, Optional, Tuple, Type, Union
+from typing import (
+    Any,
+    Callable,
+    ForwardRef,
+    List,
+    Optional,
+    Tuple,
+    Type,
+    Union,
+)
 
 from mashumaro.core.const import PY_39_MIN, PY_311_MIN
 from mashumaro.core.meta.code.lines import CodeLines
@@ -405,10 +414,15 @@ def pack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]:
             return f"*{packer}"
         elif is_type_var_tuple(spec.type):
             return PackerRegistry.get(spec.copy(type=Tuple[Any, ...]))
-        else:
-            raise UnserializableDataError(
-                f"{spec.type} as a field type is not supported by mashumaro"
+        elif isinstance(spec.type, ForwardRef):
+            evaluated = spec.builder.evaluate_forward_ref(
+                spec.type, spec.owner
             )
+            if evaluated is not None:
+                return PackerRegistry.get(spec.copy(type=evaluated))
+        raise UnserializableDataError(
+            f"{spec.type} as a field type is not supported by mashumaro"
+        )
 
 
 @register
@@ -601,6 +615,7 @@ def pack_typed_dict(spec: ValueSpec) -> Expression:
                     type=annotations[key],
                     expression=f"value['{key}']",
                     could_be_none=True,
+                    owner=spec.type,
                 )
             )
             lines.append(f"d['{key}'] = {packer}")
@@ -612,6 +627,7 @@ def pack_typed_dict(spec: ValueSpec) -> Expression:
                         type=annotations[key],
                         expression="key_value",
                         could_be_none=True,
+                        owner=spec.type,
                     )
                 )
                 lines.append(f"d['{key}'] = {packer}")
diff --git a/mashumaro/core/meta/types/unpack.py b/mashumaro/core/meta/types/unpack.py
index 5c502a6..97d5769 100644
--- a/mashumaro/core/meta/types/unpack.py
+++ b/mashumaro/core/meta/types/unpack.py
@@ -14,7 +14,17 @@ from contextlib import suppress
 from dataclasses import is_dataclass
 from decimal import Decimal
 from fractions import Fraction
-from typing import Any, Callable, Iterable, List, Optional, Tuple, Type, Union
+from typing import (
+    Any,
+    Callable,
+    ForwardRef,
+    Iterable,
+    List,
+    Optional,
+    Tuple,
+    Type,
+    Union,
+)
 
 from mashumaro.core.const import PY_39_MIN, PY_311_MIN
 from mashumaro.core.helpers import parse_timezone
@@ -706,10 +716,15 @@ def unpack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]:
             return f"*{unpacker}"
         elif is_type_var_tuple(spec.type):
             return UnpackerRegistry.get(spec.copy(type=Tuple[Any, ...]))
-        else:
-            raise UnserializableDataError(
-                f"{spec.type} as a field type is not supported by mashumaro"
+        elif isinstance(spec.type, ForwardRef):
+            evaluated = spec.builder.evaluate_forward_ref(
+                spec.type, spec.owner
             )
+            if evaluated is not None:
+                return UnpackerRegistry.get(spec.copy(type=evaluated))
+        raise UnserializableDataError(
+            f"{spec.type} as a field type is not supported by mashumaro"
+        )
 
 
 @register
@@ -989,6 +1004,7 @@ def unpack_typed_dict(spec: ValueSpec) -> Expression:
                     type=annotations[key],
                     expression=f"value['{key}']",
                     could_be_none=True,
+                    owner=spec.type,
                 )
             )
             lines.append(f"d['{key}'] = {unpacker}")
@@ -1000,6 +1016,7 @@ def unpack_typed_dict(spec: ValueSpec) -> Expression:
                         type=annotations[key],
                         expression="key_value",
                         could_be_none=True,
+                        owner=spec.type,
                     )
                 )
                 lines.append(f"d['{key}'] = {unpacker}")


Test patch:
diff --git a/tests/test_forward_refs/test_typed_dict_as_forward_ref/__init__.py b/tests/test_forward_refs/test_typed_dict_as_forward_ref/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_forward_refs/test_typed_dict_as_forward_ref/bar.py b/tests/test_forward_refs/test_typed_dict_as_forward_ref/bar.py
new file mode 100644
index 0000000..988ec9c
--- /dev/null
+++ b/tests/test_forward_refs/test_typed_dict_as_forward_ref/bar.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+
+class Bar(TypedDict):
+    baz: str
diff --git a/tests/test_forward_refs/test_typed_dict_as_forward_ref/foo.py b/tests/test_forward_refs/test_typed_dict_as_forward_ref/foo.py
new file mode 100644
index 0000000..9830af5
--- /dev/null
+++ b/tests/test_forward_refs/test_typed_dict_as_forward_ref/foo.py
@@ -0,0 +1,9 @@
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from .bar import Bar
+
+
+class Foo(TypedDict):
+    bar: Bar
diff --git a/tests/test_forward_refs/test_typed_dict_as_forward_ref/test_foobar_1.py b/tests/test_forward_refs/test_typed_dict_as_forward_ref/test_foobar_1.py
new file mode 100644
index 0000000..6a512e8
--- /dev/null
+++ b/tests/test_forward_refs/test_typed_dict_as_forward_ref/test_foobar_1.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from mashumaro import DataClassDictMixin
+
+from .foo import Foo
+
+
+@dataclass
+class FooBar(DataClassDictMixin):
+    foo: Foo
+
+
+def test_foobar():
+    obj = FooBar.from_dict({"foo": {"bar": {"baz": "baz"}}})
+    assert obj.foo["bar"]["baz"] == "baz"
+    assert obj.to_dict() == {"foo": {"bar": {"baz": "baz"}}}
diff --git a/tests/test_forward_refs/test_typed_dict_as_forward_ref/test_foobar_2.py b/tests/test_forward_refs/test_typed_dict_as_forward_ref/test_foobar_2.py
new file mode 100644
index 0000000..a98e227
--- /dev/null
+++ b/tests/test_forward_refs/test_typed_dict_as_forward_ref/test_foobar_2.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from typing_extensions import TypedDict
+
+from mashumaro import DataClassDictMixin
+
+
+class Bar(TypedDict):
+    baz: str
+
+
+class Foo(TypedDict):
+    bar: Bar
+
+
+@dataclass
+class FooBar(DataClassDictMixin):
+    foo: Foo
+
+
+def test_foobar():
+    obj = FooBar.from_dict({"foo": {"bar": {"baz": "baz"}}})
+    assert obj.foo["bar"]["baz"] == "baz"
+    assert obj.to_dict() == {"foo": {"bar": {"baz": "baz"}}}