Repository: strawberry-graphql/strawberry

Issue:
Pydantic  - Usage of nested models with explicitly defined types/inputs- regression in Strawberry 0.92.0
Morning, 

Thanks for the awesome library! :+1: 

As of Strawberry 0.92 defining both, an input and a type for a nested pydantic model produces a confusion error on QUERY side:

>> Expected value of type 'AResponse' but got: <BInput instance>.

e.g. 

```python 
@strawberry.experimental.pydantic.input(model=A)                                                                                                                  
class AInput:                                                                                                                                                     
    a: strawberry.auto                                                                                                                                            
                                                                                                                                                                  
@strawberry.experimental.pydantic.input(model=B)                                                                                                                  
class BInput:                                                                                                                                                     
    b: strawberry.auto                                                                                                                                            
    a: strawberry.auto                                                                                                                                            
                                                                                                                                                                  
@strawberry.experimental.pydantic.type(model=A)                                                                                                                   
class AResponse:                                                                                                                                                     
     a: strawberry.auto                                                                                                                                            
                                                                                                                                                                  
@strawberry.experimental.pydantic.type(model=B)                                                                                                                   
class BResponse:                                                                                                                                                  
    b: strawberry.auto                                                                                                                                            
    a: strawberry.auto                                                                                                                                            
   
```

This may be a regression in behavior(the error got caught and exposed), rather than implementation.

Would be willing to contribute a PR if you could confirm that this is unexpected behaviour and not something that is intentional awaiting a rework down the line. Have a great weekend!



Discussion:
@bbsbb can you provide a full example with the query you used to get the error? 😊
Reproduction:

https://github.com/bbsbb/strawberry-graphql-1504

Reason: https://github.com/bbsbb/strawberry-graphql-1504/blob/main/strawberry_graphql_1504/schema.py#L7

Order was not significant pre 0.92. Let me know if anything else is needed.


@patrick91  - Morning! Just wondering if you have had a moment to take a look and consider if this would be addressed?
I think I introduced this with one of my changes, I'm working on a fix now. It has to do with the way strawberry registers it's types.

Resolving patch:
diff --git a/strawberry/experimental/pydantic/conversion.py b/strawberry/experimental/pydantic/conversion.py
--- a/strawberry/experimental/pydantic/conversion.py
+++ b/strawberry/experimental/pydantic/conversion.py
@@ -45,6 +45,7 @@ def _convert_from_pydantic_to_strawberry_type(
     if hasattr(type_, "_type_definition"):
         # in the case of an interface, the concrete type may be more specific
         # than the type in the field definition
+        # don't check _strawberry_input_type because inputs can't be interfaces
         if hasattr(type(data), "_strawberry_type"):
             type_ = type(data)._strawberry_type
         return convert_pydantic_model_to_strawberry_class(
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
@@ -44,14 +44,14 @@
 from .exceptions import MissingFieldsListError, UnregisteredTypeException
 
 
-def replace_pydantic_types(type_: Any):
+def replace_pydantic_types(type_: Any, is_input: bool):
     origin = getattr(type_, "__origin__", None)
     if origin is Literal:
         # Literal does not have types in its __args__ so we return early
         return type_
     if hasattr(type_, "__args__"):
         replaced_type = type_.copy_with(
-            tuple(replace_pydantic_types(t) for t in type_.__args__)
+            tuple(replace_pydantic_types(t, is_input) for t in type_.__args__)
         )
 
         if isinstance(replaced_type, TypeDefinition):
@@ -67,18 +67,19 @@ def replace_pydantic_types(type_: Any):
         return replaced_type
 
     if issubclass(type_, BaseModel):
-        if hasattr(type_, "_strawberry_type"):
-            return type_._strawberry_type
+        attr = "_strawberry_input_type" if is_input else "_strawberry_type"
+        if hasattr(type_, attr):
+            return getattr(type_, attr)
         else:
             raise UnregisteredTypeException(type_)
 
     return type_
 
 
-def get_type_for_field(field: ModelField):
+def get_type_for_field(field: ModelField, is_input: bool):
     type_ = field.outer_type_
     type_ = get_basic_type(type_)
-    type_ = replace_pydantic_types(type_)
+    type_ = replace_pydantic_types(type_, is_input)
 
     if not field.required:
         type_ = Optional[type_]
@@ -138,14 +139,14 @@ def wrap(cls: Any) -> Type[StrawberryTypeFromPydantic[PydanticModel]]:
         all_model_fields: List[DataclassCreationFields] = [
             DataclassCreationFields(
                 name=field_name,
-                type_annotation=get_type_for_field(field),
+                type_annotation=get_type_for_field(field, is_input),
                 field=StrawberryField(
                     python_name=field.name,
                     graphql_name=field.alias if field.has_alias else None,
                     # always unset because we use default_factory instead
                     default=UNSET,
                     default_factory=get_default_factory_for_field(field),
-                    type_annotation=get_type_for_field(field),
+                    type_annotation=get_type_for_field(field, is_input),
                     description=field.field_info.description,
                 ),
             )
@@ -194,7 +195,10 @@ def is_type_of(cls: Type, obj: Any, _info: GraphQLResolveInfo) -> bool:
             directives=directives,
         )
 
-        model._strawberry_type = cls  # type: ignore
+        if is_input:
+            model._strawberry_input_type = cls  # type: ignore
+        else:
+            model._strawberry_type = cls  # type: ignore
         cls._pydantic_type = model  # type: ignore
 
         def from_pydantic(


Test patch:
diff --git a/tests/experimental/pydantic/test_basic.py b/tests/experimental/pydantic/test_basic.py
--- a/tests/experimental/pydantic/test_basic.py
+++ b/tests/experimental/pydantic/test_basic.py
@@ -159,6 +159,31 @@ class UserType:
             group: strawberry.auto
 
 
+def test_referencing_other_input_models_fails_when_not_registered():
+    class Group(pydantic.BaseModel):
+        name: str
+
+    class User(pydantic.BaseModel):
+        age: int
+        password: Optional[str]
+        group: Group
+
+    @strawberry.experimental.pydantic.type(Group)
+    class GroupType:
+        name: strawberry.auto
+
+    with pytest.raises(
+        strawberry.experimental.pydantic.UnregisteredTypeException,
+        match=("Cannot find a Strawberry Type for (.*) did you forget to register it?"),
+    ):
+
+        @strawberry.experimental.pydantic.input(User)
+        class UserInputType:
+            age: strawberry.auto
+            password: strawberry.auto
+            group: strawberry.auto
+
+
 def test_referencing_other_registered_models():
     class Group(pydantic.BaseModel):
         name: str
@@ -539,3 +564,97 @@ class UserType:
 
     assert field2.python_name == "interface_field"
     assert field2.type is BaseType
+
+
+def test_both_output_and_input_type():
+    class Work(pydantic.BaseModel):
+        time: float
+
+    class User(pydantic.BaseModel):
+        name: str
+        work: Optional[Work]
+
+    class Group(pydantic.BaseModel):
+        users: List[User]
+
+    # Test both definition orders
+    @strawberry.experimental.pydantic.input(Work)
+    class WorkInput:
+        time: strawberry.auto
+
+    @strawberry.experimental.pydantic.type(Work)
+    class WorkOutput:
+        time: strawberry.auto
+
+    @strawberry.experimental.pydantic.type(User)
+    class UserOutput:
+        name: strawberry.auto
+        work: strawberry.auto
+
+    @strawberry.experimental.pydantic.input(User)
+    class UserInput:
+        name: strawberry.auto
+        work: strawberry.auto
+
+    @strawberry.experimental.pydantic.input(Group)
+    class GroupInput:
+        users: strawberry.auto
+
+    @strawberry.experimental.pydantic.type(Group)
+    class GroupOutput:
+        users: strawberry.auto
+
+    @strawberry.type
+    class Query:
+        groups: List[GroupOutput]
+
+    @strawberry.type
+    class Mutation:
+        @strawberry.mutation
+        def updateGroup(group: GroupInput) -> GroupOutput:
+            pass
+
+    # This triggers the exception from #1504
+    schema = strawberry.Schema(query=Query, mutation=Mutation)
+    expected_schema = """
+input GroupInput {
+  users: [UserInput!]!
+}
+
+type GroupOutput {
+  users: [UserOutput!]!
+}
+
+type Mutation {
+  updateGroup(group: GroupInput!): GroupOutput!
+}
+
+type Query {
+  groups: [GroupOutput!]!
+}
+
+input UserInput {
+  name: String!
+  work: WorkInput = null
+}
+
+type UserOutput {
+  name: String!
+  work: WorkOutput
+}
+
+input WorkInput {
+  time: Float!
+}
+
+type WorkOutput {
+  time: Float!
+}"""
+    assert schema.as_str().strip() == expected_schema.strip()
+
+    assert Group._strawberry_type == GroupOutput
+    assert Group._strawberry_input_type == GroupInput
+    assert User._strawberry_type == UserOutput
+    assert User._strawberry_input_type == UserInput
+    assert Work._strawberry_type == WorkOutput
+    assert Work._strawberry_input_type == WorkInput
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
@@ -910,3 +910,56 @@ def age() -> int:
     assert user.password == "abc"
     assert User._type_definition.fields[0].base_resolver() == 84
     assert User._type_definition.fields[1].base_resolver() == 42
+
+
+def test_can_convert_both_output_and_input_type():
+    class Work(BaseModel):
+        time: float
+
+    class User(BaseModel):
+        name: str
+        work: Optional[Work]
+
+    class Group(BaseModel):
+        users: List[User]
+
+    # Test both definition orders
+    @strawberry.experimental.pydantic.input(Work)
+    class WorkInput:
+        time: strawberry.auto
+
+    @strawberry.experimental.pydantic.type(Work)
+    class WorkOutput:
+        time: strawberry.auto
+
+    @strawberry.experimental.pydantic.type(User)
+    class UserOutput:
+        name: strawberry.auto
+        work: strawberry.auto
+
+    @strawberry.experimental.pydantic.input(User)
+    class UserInput:
+        name: strawberry.auto
+        work: strawberry.auto
+
+    @strawberry.experimental.pydantic.input(Group)
+    class GroupInput:
+        users: strawberry.auto
+
+    @strawberry.experimental.pydantic.type(Group)
+    class GroupOutput:
+        users: strawberry.auto
+
+    origin_group = Group(
+        users=[
+            User(name="Alice", work=Work(time=10.0)),
+            User(name="Bob", work=Work(time=5.0)),
+        ]
+    )
+    group = GroupOutput.from_pydantic(origin_group)
+    final_group = group.to_pydantic()
+    assert origin_group == final_group
+
+    group_input = GroupInput.from_pydantic(origin_group)
+    final_group = group_input.to_pydantic()
+    assert origin_group == final_group