Repository: strawberry-graphql/strawberry

Issue:
Pydantic models as a default values in inputs
When using `strawberry.experimental.pydantic.type`, default values of Pydantic models are not considered, if they are Pydantic models themselves.

## Describe the Bug

Let's have an example:

```python
import strawberry
from pydantic import BaseModel


class Test(BaseModel):
    id: int = 10


class Test2(BaseModel):
    test: Test = Test()
    id: int = 10


@strawberry.experimental.pydantic.type(model=Test, all_fields=True)
class StrawberryTest:
    ...


@strawberry.experimental.pydantic.type(model=Test2, all_fields=True)
class StrawberryTest2:
    ...


@strawberry.experimental.pydantic.type(model=Test, all_fields=True, is_input=True)
class StrawberryTestInput:
    ...


@strawberry.experimental.pydantic.type(model=Test2, all_fields=True, is_input=True)
class StrawberryTest2Input:
    ...


@strawberry.type(is_input=True)
class StrawberryTestPlainInput:
    id: int = 10


@strawberry.type(is_input=True)
class StrawberryTestPlain2Input:
    test: StrawberryTestPlainInput = StrawberryTestPlainInput()
    id: int = 10


@strawberry.type
class Query:
    @strawberry.field()
    def test(self, x: StrawberryTestInput) -> int:
        return 42

    @strawberry.field()
    def test2(self, x: StrawberryTest2Input) -> int:
        return 42

    @strawberry.field()
    def testplain2(self, x: StrawberryTestPlain2Input) -> int:
        return 42


schema = strawberry.Schema(query=Query)

print(schema)
```

The generated schema for `StrawberryTestPlain2Input` is 

```
input StrawberryTestPlain2Input {
  test: StrawberryTestPlainInput! = {id: 10}
  id: Int! = 10
}
```

Which is correct. But for `StrawberryTest2Input`, the schema is 

```
input StrawberryTest2Input {
  test: StrawberryTestInput!
  id: Int! = 10
}
```

This is incorrect because it says that the `test` field is mandatory, although it has a default value and therefore isn't mandatory.

I would expect that the `test` field of `StrawberryTest2Input` will be either nullable or that the default value will be provided in the same way as in the `StrawberryTestPlain2Input`.

## System Information

 - Operating system: macOS Ventura 
 - Strawberry version (if applicable): 0.215.2
 - Python version: 3.10.10

## Additional Context

I wonder if this is expected behaviour and can be somehow fixed on my side or a bug.


<!-- POLAR PLEDGE BADGE START -->
## Upvote & Fund

- We're using [Polar.sh](https://polar.sh/strawberry-graphql) so you can upvote and help fund this issue.
- We receive the funding once the issue is completed & confirmed by you.
- Thank you in advance for helping prioritize & fund our backlog.

<a href="https://polar.sh/strawberry-graphql/strawberry/issues/3285">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/3285/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/3285/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/strawberry/experimental/pydantic/_compat.py b/strawberry/experimental/pydantic/_compat.py
--- a/strawberry/experimental/pydantic/_compat.py
+++ b/strawberry/experimental/pydantic/_compat.py
@@ -162,6 +162,9 @@ def get_basic_type(self, type_: Any) -> Type[Any]:
 
         return type_
 
+    def model_dump(self, model_instance: BaseModel) -> Dict[Any, Any]:
+        return model_instance.model_dump()
+
 
 class PydanticV1Compat:
     @property
@@ -235,6 +238,9 @@ def get_basic_type(self, type_: Any) -> Type[Any]:
 
         return type_
 
+    def model_dump(self, model_instance: BaseModel) -> Dict[Any, Any]:
+        return model_instance.dict()
+
 
 class PydanticCompat:
     def __init__(self, is_v2: bool) -> None:
diff --git a/strawberry/experimental/pydantic/object_type.py b/strawberry/experimental/pydantic/object_type.py
--- a/strawberry/experimental/pydantic/object_type.py
+++ b/strawberry/experimental/pydantic/object_type.py
@@ -91,7 +91,7 @@ def _build_dataclass_creation_fields(
             graphql_name=graphql_name,
             # always unset because we use default_factory instead
             default=dataclasses.MISSING,
-            default_factory=get_default_factory_for_field(field),
+            default_factory=get_default_factory_for_field(field, compat=compat),
             type_annotation=StrawberryAnnotation.from_annotation(field_type),
             description=field.description,
             deprecation_reason=(
diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py
--- a/strawberry/experimental/pydantic/utils.py
+++ b/strawberry/experimental/pydantic/utils.py
@@ -13,6 +13,8 @@
     cast,
 )
 
+from pydantic import BaseModel
+
 from strawberry.experimental.pydantic._compat import (
     CompatModelField,
     PydanticCompat,
@@ -33,7 +35,6 @@
 )
 
 if TYPE_CHECKING:
-    from pydantic import BaseModel
     from pydantic.typing import NoArgAnyCallable
 
 
@@ -72,6 +73,7 @@ def to_tuple(self) -> Tuple[str, Type, dataclasses.Field]:
 
 def get_default_factory_for_field(
     field: CompatModelField,
+    compat: PydanticCompat,
 ) -> Union[NoArgAnyCallable, dataclasses._MISSING_TYPE]:
     """
     Gets the default factory for a pydantic field.
@@ -105,9 +107,14 @@ def get_default_factory_for_field(
         return default_factory
 
     # if we have a default, we should return it
-
     if has_default:
-        return lambda: smart_deepcopy(default)
+        # if the default value is a pydantic base model
+        # we should return the serialized version of that default for
+        # printing the value.
+        if isinstance(default, BaseModel):
+            return lambda: compat.model_dump(default)
+        else:
+            return lambda: smart_deepcopy(default)
 
     # if we don't have default or default_factory, but the field is not required,
     # we should return a factory that returns None


Test patch:
diff --git a/tests/experimental/pydantic/schema/test_defaults.py b/tests/experimental/pydantic/schema/test_defaults.py
--- a/tests/experimental/pydantic/schema/test_defaults.py
+++ b/tests/experimental/pydantic/schema/test_defaults.py
@@ -183,3 +183,83 @@ def a(self) -> PydanticUser:
     """
 
     assert print_schema(schema) == textwrap.dedent(expected).strip()
+
+
+def test_v2_input_with_nonscalar_default():
+    class NonScalarType(pydantic.BaseModel):
+        id: int = 10
+        nullable_field: Optional[int] = None
+
+    class Owning(pydantic.BaseModel):
+        non_scalar_type: NonScalarType = NonScalarType()
+        id: int = 10
+
+    @strawberry.experimental.pydantic.type(
+        model=NonScalarType, all_fields=True, is_input=True
+    )
+    class NonScalarTypeInput: ...
+
+    @strawberry.experimental.pydantic.type(model=Owning, all_fields=True, is_input=True)
+    class OwningInput: ...
+
+    @strawberry.type
+    class ExampleOutput:
+        owning_id: int
+        non_scalar_id: int
+        non_scalar_nullable_field: Optional[int]
+
+    @strawberry.type
+    class Query:
+        @strawberry.field()
+        def test(self, x: OwningInput) -> ExampleOutput:
+            return ExampleOutput(
+                owning_id=x.id,
+                non_scalar_id=x.non_scalar_type.id,
+                non_scalar_nullable_field=x.non_scalar_type.nullable_field,
+            )
+
+    schema = strawberry.Schema(Query)
+
+    expected = """
+    type ExampleOutput {
+      owningId: Int!
+      nonScalarId: Int!
+      nonScalarNullableField: Int
+    }
+
+    input NonScalarTypeInput {
+      id: Int! = 10
+      nullableField: Int = null
+    }
+
+    input OwningInput {
+      nonScalarType: NonScalarTypeInput! = {id: 10}
+      id: Int! = 10
+    }
+
+    type Query {
+      test(x: OwningInput!): ExampleOutput!
+    }
+    """
+
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
+
+    query = """
+    query($input_data: OwningInput!)
+    {
+        test(x: $input_data) {
+            owningId nonScalarId nonScalarNullableField
+        }
+    }
+    """
+    result = schema.execute_sync(
+        query, variable_values=dict(input_data=dict(nonScalarType={}))
+    )
+
+    assert not result.errors
+    expected_result = {
+        "owningId": 10,
+        "nonScalarId": 10,
+        "nonScalarNullableField": None,
+    }
+    assert result.data["test"] == expected_result
diff --git a/tests/experimental/pydantic/test_conversion.py b/tests/experimental/pydantic/test_conversion.py
--- a/tests/experimental/pydantic/test_conversion.py
+++ b/tests/experimental/pydantic/test_conversion.py
@@ -12,8 +12,7 @@
 from strawberry.experimental.pydantic._compat import (
     IS_PYDANTIC_V2,
     CompatModelField,
-    PydanticV1Compat,
-    PydanticV2Compat,
+    PydanticCompat,
 )
 from strawberry.experimental.pydantic.exceptions import (
     AutoFieldsNotInBaseModelError,
@@ -841,10 +840,11 @@ class UserType:
 
 
 def test_get_default_factory_for_field():
-    if IS_PYDANTIC_V2:
-        MISSING_TYPE = PydanticV2Compat().PYDANTIC_MISSING_TYPE
-    else:
-        MISSING_TYPE = PydanticV1Compat().PYDANTIC_MISSING_TYPE
+    class User(BaseModel):
+        pass
+
+    compat = PydanticCompat.from_model(User)
+    MISSING_TYPE = compat.PYDANTIC_MISSING_TYPE
 
     def _get_field(
         default: Any = MISSING_TYPE,
@@ -867,7 +867,7 @@ def _get_field(
 
     field = _get_field()
 
-    assert get_default_factory_for_field(field) is dataclasses.MISSING
+    assert get_default_factory_for_field(field, compat) is dataclasses.MISSING
 
     def factory_func():
         return "strawberry"
@@ -875,13 +875,13 @@ def factory_func():
     field = _get_field(default_factory=factory_func)
 
     # should return the default_factory unchanged
-    assert get_default_factory_for_field(field) is factory_func
+    assert get_default_factory_for_field(field, compat) is factory_func
 
     mutable_default = [123, "strawberry"]
 
     field = _get_field(mutable_default)
 
-    created_factory = get_default_factory_for_field(field)
+    created_factory = get_default_factory_for_field(field, compat)
 
     # should return a factory that copies the default parameter
     assert created_factory() == mutable_default
@@ -893,7 +893,7 @@ def factory_func():
         BothDefaultAndDefaultFactoryDefinedError,
         match=("Not allowed to specify both default and default_factory."),
     ):
-        get_default_factory_for_field(field)
+        get_default_factory_for_field(field, compat)
 
 
 def test_convert_input_types_to_pydantic_default_and_default_factory():
diff --git a/tests/schema/test_input.py b/tests/schema/test_input.py
--- a/tests/schema/test_input.py
+++ b/tests/schema/test_input.py
@@ -1,6 +1,8 @@
+import textwrap
 from typing import Optional
 
 import strawberry
+from strawberry.printer import print_schema
 
 
 def test_renaming_input_fields():
@@ -27,3 +29,74 @@ def filter(self, input: FilterInput) -> str:
     assert not result.errors
     assert result.data
     assert result.data["filter"] == "Hello nope"
+
+
+def test_input_with_nonscalar_field_default():
+    @strawberry.input
+    class NonScalarField:
+        id: int = 10
+        nullable_field: Optional[int] = None
+
+    @strawberry.input
+    class Input:
+        non_scalar_field: NonScalarField = strawberry.field(
+            default_factory=lambda: NonScalarField()
+        )
+        id: int = 10
+
+    @strawberry.type
+    class ExampleOutput:
+        input_id: int
+        non_scalar_id: int
+        non_scalar_nullable_field: Optional[int]
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def example(self, data: Input) -> ExampleOutput:
+            return ExampleOutput(
+                input_id=data.id,
+                non_scalar_id=data.non_scalar_field.id,
+                non_scalar_nullable_field=data.non_scalar_field.nullable_field,
+            )
+
+    schema = strawberry.Schema(query=Query)
+
+    expected = """
+    type ExampleOutput {
+      inputId: Int!
+      nonScalarId: Int!
+      nonScalarNullableField: Int
+    }
+
+    input Input {
+      nonScalarField: NonScalarField! = {id: 10}
+      id: Int! = 10
+    }
+
+    input NonScalarField {
+      id: Int! = 10
+      nullableField: Int = null
+    }
+
+    type Query {
+      example(data: Input!): ExampleOutput!
+    }
+    """
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
+
+    query = """
+    query($input_data: Input!)
+    {
+        example(data: $input_data) {
+            inputId nonScalarId nonScalarNullableField
+        }
+    }
+    """
+    result = schema.execute_sync(
+        query, variable_values=dict(input_data=dict(nonScalarField={}))
+    )
+
+    assert not result.errors
+    expected_result = {"inputId": 10, "nonScalarId": 10, "nonScalarNullableField": None}
+    assert result.data["example"] == expected_result