Repository: litestar-org/polyfactory (python)

Issue:
Bug: Unsupported type: ForwardRef('JsonValue') in v2.22.0
### Description

Doing the attached example now raises an 

```
ParameterException: Unsupported type: ForwardRef('JsonValue') on field '' from class TestModelFactory.
```

which was working ok in 2.21.0.

I'm not sure if this is an intended regression or not? I couldn't see anything obvious in the release notes.

### URL to code causing the issue

_No response_

### MCVE

```python
from pydantic import BaseModel, JsonValue
from polyfactory.factories.pydantic_factory import ModelFactory

class TestModel(BaseModel):
    bar: dict[str, JsonValue]

class TestModelFactory(ModelFactory[TestModel]): ...

TestModelFactory.build()
```

### Steps to reproduce

```bash
1. Run the code snipped provided on v2.22.0 (Python 3.10).
```

### Screenshots

"In the format of: `![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)`"


### Logs

```bash
---------------------------------------------------------------------------
ParameterException                        Traceback (most recent call last)
Cell In[4], line 1
----> 1 TestModelFactory.build()

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/pydantic_factory.py:538, in ModelFactory.build(cls, factory_use_construct, **kwargs)
    532 if "_build_context" not in kwargs:
    533     kwargs["_build_context"] = PydanticBuildContext(
    534         seen_models=set(),
    535         factory_use_construct=factory_use_construct,
    536     )
--> 538 processed_kwargs = cls.process_kwargs(**kwargs)
    540 return cls._create_model(kwargs["_build_context"], **processed_kwargs)

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/base.py:1080, in BaseFactory.process_kwargs(cls, **kwargs)
   1073     result[field_meta.name] = cls._handle_factory_field(
   1074         field_value=field_value,
   1075         field_build_parameters=field_build_parameters,
   1076         build_context=_build_context,
   1077     )
   1078     continue
-> 1080 field_result = cls.get_field_value(
   1081     field_meta,
   1082     field_build_parameters=field_build_parameters,
   1083     build_context=_build_context,
   1084 )
   1085 if field_result is Null:
   1086     continue

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/pydantic_factory.py:511, in ModelFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    509     result = cls.__random__.choice(field_meta.examples)
    510 else:
--> 511     result = super().get_field_value(
    512         field_meta=field_meta, field_build_parameters=field_build_parameters, build_context=build_context
    513     )
    514 return result

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/base.py:849, in BaseFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    830             return handle_constrained_collection(
    831                 collection_type=collection_type,  # type: ignore[type-var]
    832                 factory=cls,
   (...)
    838                 build_context=build_context,
    839             )
    840         return handle_constrained_mapping(
    841             factory=cls,
    842             field_meta=field_meta,
   (...)
    846             build_context=build_context,
    847         )
--> 849     return handle_collection_type(
    850         field_meta,
    851         origin,
    852         cls,
    853         field_build_parameters=field_build_parameters,
    854         build_context=build_context,
    855     )
    857 provider_map = cls.get_provider_map()
    858 if provider := (provider_map.get(field_meta.annotation) or provider_map.get(unwrapped_annotation)):

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/value_generators/complex_types.py:51, in handle_collection_type(field_meta, container_type, factory, field_build_parameters, build_context)
     44 for key_field_meta, value_field_meta in cast(
     45     "Iterable[Tuple[FieldMeta, FieldMeta]]",
     46     zip(field_meta.children[::2], field_meta.children[1::2]),
     47 ):
     48     key = factory.get_field_value(
     49         key_field_meta, field_build_parameters=field_build_parameters, build_context=build_context
     50     )
---> 51     value = factory.get_field_value(
     52         value_field_meta, field_build_parameters=field_build_parameters, build_context=build_context
     53     )
     54     container[key] = value
     55 return container

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/pydantic_factory.py:511, in ModelFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    509     result = cls.__random__.choice(field_meta.examples)
    510 else:
--> 511     result = super().get_field_value(
    512         field_meta=field_meta, field_build_parameters=field_build_parameters, build_context=build_context
    513     )
    514 return result

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/base.py:795, in BaseFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    792     if not children:
    793         return None
--> 795     return cls.get_field_value(cls.__random__.choice(children), field_build_parameters, build_context)
    797 if BaseFactory.is_factory_type(annotation=unwrapped_annotation):
    798     if not field_build_parameters and unwrapped_annotation in build_context["seen_models"]:

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/pydantic_factory.py:511, in ModelFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    509     result = cls.__random__.choice(field_meta.examples)
    510 else:
--> 511     result = super().get_field_value(
    512         field_meta=field_meta, field_build_parameters=field_build_parameters, build_context=build_context
    513     )
    514 return result

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/base.py:849, in BaseFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    830             return handle_constrained_collection(
    831                 collection_type=collection_type,  # type: ignore[type-var]
    832                 factory=cls,
   (...)
    838                 build_context=build_context,
    839             )
    840         return handle_constrained_mapping(
    841             factory=cls,
    842             field_meta=field_meta,
   (...)
    846             build_context=build_context,
    847         )
--> 849     return handle_collection_type(
    850         field_meta,
    851         origin,
    852         cls,
    853         field_build_parameters=field_build_parameters,
    854         build_context=build_context,
    855     )
    857 provider_map = cls.get_provider_map()
    858 if provider := (provider_map.get(field_meta.annotation) or provider_map.get(unwrapped_annotation)):

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/value_generators/complex_types.py:59, in handle_collection_type(field_meta, container_type, factory, field_build_parameters, build_context)
     55     return container
     57 if issubclass(container_type, MutableSequence):
     58     container.extend(
---> 59         [
     60             factory.get_field_value(
     61                 subfield_meta, field_build_parameters=field_build_parameters, build_context=build_context
     62             )
     63             for subfield_meta in field_meta.children
     64         ]
     65     )
     66     return container
     68 if issubclass(container_type, Set):

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/value_generators/complex_types.py:60, in <listcomp>(.0)
     55     return container
     57 if issubclass(container_type, MutableSequence):
     58     container.extend(
     59         [
---> 60             factory.get_field_value(
     61                 subfield_meta, field_build_parameters=field_build_parameters, build_context=build_context
     62             )
     63             for subfield_meta in field_meta.children
     64         ]
     65     )
     66     return container
     68 if issubclass(container_type, Set):

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/pydantic_factory.py:511, in ModelFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    509     result = cls.__random__.choice(field_meta.examples)
    510 else:
--> 511     result = super().get_field_value(
    512         field_meta=field_meta, field_build_parameters=field_build_parameters, build_context=build_context
    513     )
    514 return result

File /opt/homebrew/Caskroom/mambaforge/base/envs/myenv/lib/python3.10/site-packages/polyfactory/factories/base.py:875, in BaseFactory.get_field_value(cls, field_meta, field_build_parameters, build_context)
    868         return unwrapped_annotation()
    870 msg = (
    871     f"Unsupported type: {unwrapped_annotation!r} on field '{field_meta.name}' from class {cls.__name__}."
    872     "\n\nEither use 'add_provider', extend the providers map, or add a factory function for the field on the model."
    873 )
--> 875 raise ParameterException(
    876     msg,
    877 )

ParameterException: Unsupported type: ForwardRef('JsonValue') on field '' from class TestModelFactory.

Either use 'add_provider', extend the providers map, or add a factory function for the field on the model.
```

### Release Version

v2.22.0

### Platform

- [ ] Linux
- [x] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

Pull request:
fix: handle recursive types that use ForwardRef
<!--
By submitting this pull request, you agree to:
- follow [Litestar's Code of Conduct](https://github.com/litestar-org/.github/blob/main/CODE_OF_CONDUCT.md)
- follow [Litestar's contribution guidelines](https://github.com/litestar-org/.github/blob/main/CONTRIBUTING.md)
- follow the [PSFs's Code of Conduct](https://www.python.org/psf/conduct/)
-->
## Description

- Handling of union and recursive types changed slightly with https://github.com/litestar-org/polyfactory/pulls?q=sort%3Aupdated-desc+is%3Apr+is%3Aclosed
- Update to handle forward ref explicitly

<!--
Please add in issue numbers this pull request will close, if applicable
Examples: Fixes #4321 or Closes #1234

Ensure you are using a supported keyword to properly link an issue:
https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
-->
## Closes

Closes #725 

Patch:
diff --git a/docs/usage/configuration.rst b/docs/usage/configuration.rst
index 702a5cc..9419025 100644
--- a/docs/usage/configuration.rst
+++ b/docs/usage/configuration.rst
@@ -146,3 +146,21 @@ By default, ``__use_examples__`` is set to ``False.``
 .. literalinclude:: /examples/configuration/test_example_10.py
     :caption: Use Examples Values
     :language: python
+
+
+Forward References
+------------------
+
+The ``__forward_references__`` configuration allows you to resolve forward references in type annotations.
+This is useful when dealing with recursive types that cannot be resolved automatically.
+
+By default, ``__forward_references__`` is set to an empty dictionary. You can provide a mapping of
+forward reference names to their resolved types.
+
+.. literalinclude:: /examples/configuration/test_example_11.py
+    :caption: Forward References Configuration
+    :language: python
+
+.. note::
+    The Pydantic ModelFactory has a default forward reference mapping for ``JsonValue`` to resolve to ``str``
+    to avoid recursive issues with Pydantic's JsonValue type.
diff --git a/polyfactory/factories/base.py b/polyfactory/factories/base.py
index d91fd5e..417f6db 100644
--- a/polyfactory/factories/base.py
+++ b/polyfactory/factories/base.py
@@ -67,6 +67,7 @@ from polyfactory.utils.helpers import (
 from polyfactory.utils.model_coverage import CoverageContainer, CoverageContainerCallable, resolve_kwargs_coverage
 from polyfactory.utils.predicates import (
     get_type_origin,
+    is_forward_ref,
     is_literal,
     is_optional,
     is_safe_subclass,
@@ -175,6 +176,7 @@ class BaseFactory(ABC, Generic[T]):
     """
     Flag indicating whether to use the default value on a specific field, if provided.
     """
+    __forward_references__: ClassVar[dict[str, Any]] = {}
 
     __config_keys__: tuple[str, ...] = (
         "__check_model__",
@@ -186,6 +188,7 @@ class BaseFactory(ABC, Generic[T]):
         "__min_collection_length__",
         "__max_collection_length__",
         "__use_defaults__",
+        "__forward_references__",
     )
     """Keys to be considered as config values to pass on to dynamically created factories."""
 
@@ -768,6 +771,10 @@ class BaseFactory(ABC, Generic[T]):
             return None
 
         unwrapped_annotation = unwrap_annotation(field_meta.annotation)
+        if is_forward_ref(unwrapped_annotation):
+            unwrapped_annotation = cls.__forward_references__.get(
+                unwrapped_annotation.__forward_arg__, unwrapped_annotation
+            )
 
         if is_literal(annotation=unwrapped_annotation) and (literal_args := get_args(unwrapped_annotation)):
             return cls.__random__.choice(literal_args)
@@ -896,6 +903,11 @@ class BaseFactory(ABC, Generic[T]):
             return
 
         for unwrapped_annotation in flatten_annotation(field_meta.annotation):
+            if is_forward_ref(unwrapped_annotation):
+                unwrapped_annotation = cls.__forward_references__.get(  # noqa: PLW2901
+                    unwrapped_annotation.__forward_arg__, unwrapped_annotation
+                )
+
             if unwrapped_annotation in (None, NoneType):
                 yield None
 
diff --git a/polyfactory/factories/pydantic_factory.py b/polyfactory/factories/pydantic_factory.py
index 5f6f186..af3339e 100644
--- a/polyfactory/factories/pydantic_factory.py
+++ b/polyfactory/factories/pydantic_factory.py
@@ -404,6 +404,11 @@ class ModelFactory(Generic[T], BaseFactory[T]):
     >>> payment
     Payment(amount=120, currency="EUR")
     """
+    if not _IS_PYDANTIC_V1:
+        __forward_references__: ClassVar[dict[str, Any]] = {
+            # Resolve to str to avoid recursive issues
+            "JsonValue": str,
+        }
 
     __config_keys__ = (
         *BaseFactory.__config_keys__,
diff --git a/polyfactory/field_meta.py b/polyfactory/field_meta.py
index ae154b4..d8c6110 100644
--- a/polyfactory/field_meta.py
+++ b/polyfactory/field_meta.py
@@ -8,7 +8,12 @@ from typing_extensions import get_args, get_origin
 
 from polyfactory.constants import DEFAULT_RANDOM, TYPE_MAPPING
 from polyfactory.utils.deprecation import check_for_deprecated_parameters
-from polyfactory.utils.helpers import get_annotation_metadata, is_dataclass_instance, unwrap_annotated, unwrap_new_type
+from polyfactory.utils.helpers import (
+    get_annotation_metadata,
+    is_dataclass_instance,
+    unwrap_annotated,
+    unwrap_new_type,
+)
 from polyfactory.utils.normalize_type import normalize_type
 from polyfactory.utils.predicates import is_annotated
 from polyfactory.utils.types import NoneType
diff --git a/polyfactory/utils/predicates.py b/polyfactory/utils/predicates.py
index b53bb30..b678b48 100644
--- a/polyfactory/utils/predicates.py
+++ b/polyfactory/utils/predicates.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from inspect import isclass
-from typing import Any, Literal, NewType, Optional, TypeVar, get_args
+from typing import Any, ForwardRef, Literal, NewType, Optional, TypeVar, get_args
 
 from typing_extensions import (
     Annotated,
@@ -180,3 +180,13 @@ def get_type_origin(annotation: Any) -> Any:
     if origin in (Annotated, Required, NotRequired):
         origin = get_args(annotation)[0]
     return mapped_type if (mapped_type := TYPE_MAPPING.get(origin)) else origin
+
+
+def is_forward_ref(annotation: Any) -> TypeGuard[ForwardRef]:
+    """Determine if the given type annotation is a ForwardRef.
+
+    :param annotation: A type annotation.
+
+    :returns: A boolean.
+    """
+    return isinstance(annotation, ForwardRef)


Test patch:
diff --git a/docs/examples/configuration/test_example_11.py b/docs/examples/configuration/test_example_11.py
new file mode 100644
index 0000000..d59ca72
--- /dev/null
+++ b/docs/examples/configuration/test_example_11.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import List, Union
+
+from polyfactory.factories import DataclassFactory
+
+# Define a recursive type using forward reference
+RecursiveType = Union[List["RecursiveType"], int]
+
+
+@dataclass
+class RecursiveTypeModel:
+    recursive_value: RecursiveType
+
+
+class RecursiveTypeModelFactory(DataclassFactory[RecursiveTypeModel]):
+    __forward_references__ = {"RecursiveType": int}
+
+
+def test_forward_references() -> None:
+    factory = RecursiveTypeModelFactory()
+    result = factory.build().recursive_value
+    assert isinstance(result, int) or (isinstance(result, list) and all(isinstance(value, int) for value in result))
diff --git a/tests/test_recursive_models.py b/tests/test_recursive_models.py
index a6bc4d6..13a28f0 100644
--- a/tests/test_recursive_models.py
+++ b/tests/test_recursive_models.py
@@ -1,11 +1,12 @@
 from __future__ import annotations
 
 from dataclasses import dataclass, field
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, Iterable, List, Optional, TypeVar, Union
 
 import pytest
 
 from pydantic import BaseModel, Field
+from pydantic import __version__ as pydantic_version
 
 from polyfactory.factories.dataclass_factory import DataclassFactory
 from polyfactory.factories.pydantic_factory import ModelFactory
@@ -88,3 +89,46 @@ def test_recursive_list_model() -> None:
     book_factory = DataclassFactory.create_factory(Book)
     assert book_factory.build().author.books == []
     assert book_factory.build(author=None).author is None
+
+
+@pytest.mark.skipif(pydantic_version.startswith("1"), reason="Pydantic v2+ is required for JsonValue")
+def test_recursive_type_annotation() -> None:
+    from pydantic import JsonValue
+
+    class RecursiveTypeModel(BaseModel):
+        json_value: JsonValue
+
+    factory = ModelFactory.create_factory(RecursiveTypeModel)
+    results = factory.batch(50)
+
+    valid_types = {int, str, bool, float, dict, list, type(None)}
+
+    assert _get_types(result.json_value for result in results) == valid_types
+    assert _get_types(result.json_value for result in factory.coverage()) == valid_types
+
+
+RecursiveType = Union[List["RecursiveType"], int]
+
+
+def test_recursive_model_with_forward_ref() -> None:
+    @dataclass
+    class RecursiveTypeModel:
+        json_value: RecursiveType
+
+    factory = DataclassFactory.create_factory(
+        RecursiveTypeModel,
+        __forward_references__={"RecursiveType": int},
+    )
+    results = factory.batch(50)
+
+    valid_types = {int, list}
+
+    assert _get_types(result.json_value for result in results) == valid_types
+    assert _get_types(result.json_value for result in factory.coverage()) == valid_types
+
+
+_T = TypeVar("_T")
+
+
+def _get_types(items: Iterable[_T]) -> set[type[_T]]:
+    return {type(item) for item in items}
diff --git a/tests/test_type_coverage_generation.py b/tests/test_type_coverage_generation.py
index d3861ff..77dc909 100644
--- a/tests/test_type_coverage_generation.py
+++ b/tests/test_type_coverage_generation.py
@@ -228,7 +228,7 @@ def test_coverage_optional_field() -> None:
         __model__ = OptionalInt
 
     results = list(OptionalIntFactory.coverage())
-    assert len(results) == 2
+    assert len(results) == 2, results
 
     assert isinstance(results[0].i, int)
     assert results[1].i is None