Repository: strawberry-graphql/strawberry

Issue:
buggy generic interfaces
```
import strawberry
from typing import Optional, Generic, TypeVar, List
from strawberry.scalars import JSON

GenericType = TypeVar("GenericType")

@strawberry.interface
class BlockInterface:
    id: strawberry.ID
    disclaimer: Optional[str] = strawberry.field(default=None, description='Richtext')

@strawberry.type
class JsonBlock(BlockInterface):
    data: JSON

@strawberry.type
class BlockRowType(BlockInterface, Generic[GenericType]):
    total: int
    items: List[GenericType]

@strawberry.type
class Query:
    
    @strawberry.field
    def blocks(self) -> List[BlockInterface]:
        return [
            BlockRowType(id="3", total=3, items=["a", "b", "c"]),
            BlockRowType(id="1", total=1, items=[1, 2, 3, 4]),
            JsonBlock(id="2", data={"a": 1}),
        ]

schema = strawberry.Schema(query=Query, types=[BlockRowType[int], JsonBlock, BlockRowType[str]])
```

throws: `Int cannot represent non-integer value: 'a'`

See this gists for more examples:

https://play.strawberry.rocks/?gist=e236f23fee40493f1952ad88cb05df80
https://play.strawberry.rocks/?gist=f56308e2f925eb05c37d82b4ad70efd8
https://play.strawberry.rocks/?gist=3c48b2f9e848042e87a9f9c9da143994

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



Discussion:
I guess the problem is in this [line](https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/schema/schema_converter.py#L515)

The outer Type (which implements the interface) is checked  but not the nested values in items

![image](https://github.com/strawberry-graphql/strawberry/assets/15920408/75e68604-cc98-42f4-9f29-72a328526fcf)

But the outer loop [here](https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/schema/schema_converter.py#L453) is still on the inner type
Could I assist in making progress on this issue? :)
I've done some digging in this, and we have the same issue with unions:

```python
def test_using_generics_with_union():
    @strawberry.type
    class JsonBlock:
        data: JSON

    @strawberry.type
    class BlockRowType(Generic[GenericType]):
        total: int
        items: List[GenericType]

    @strawberry.type
    class Query:
        @strawberry.field
        def blocks(
            self,
        ) -> List[Union[BlockRowType[int], BlockRowType[str], JsonBlock]]:
            return [
                BlockRowType(total=3, items=["a", "b", "c"]),
                BlockRowType(total=1, items=[1, 2, 3, 4]),
                JsonBlock(data=JSON({"a": 1})),
            ]

    schema = strawberry.Schema(query=Query)

    result = schema.execute_sync("""query {
        blocks {
            __typename
            ... on IntBlockRowType {
                a: items
            }
            ... on StrBlockRowType {
                b: items
            }
            ... on JsonBlock {
                data
            }
        }
    }""")

    assert not result.errors

    assert result.data == {
        "blocks": [
            {"id": "3", "__typename": "StrBlockRowType", "items": ["a", "b", "c"]},
            {"id": "1", "__typename": "IntBlockRowType", "items": [1, 2, 3, 4]},
            {"id": "2", "__typename": "JsonBlock", "data": {"a": 1}},
        ]
    }
```

fix this in unions, means touching this codebase: https://github.com/strawberry-graphql/strawberry/blob/29b7dd866efb30c481edcb91b67950d3adaf30ae/strawberry/union.py#L170-L224

which is separated from the interface part, so I need to think how we could combine both

Resolving patch:
diff --git a/strawberry/schema/schema_converter.py b/strawberry/schema/schema_converter.py
--- a/strawberry/schema/schema_converter.py
+++ b/strawberry/schema/schema_converter.py
@@ -31,8 +31,10 @@
     GraphQLInputObjectType,
     GraphQLInterfaceType,
     GraphQLList,
+    GraphQLNamedType,
     GraphQLNonNull,
     GraphQLObjectType,
+    GraphQLType,
     GraphQLUnionType,
     Undefined,
     ValueNode,
@@ -429,7 +431,7 @@ def from_interface(
     ) -> GraphQLInterfaceType:
         interface_name = self.config.name_converter.from_type(interface)
 
-        # Don't reevaluate known types
+        # Don't re-evaluate known types
         cached_type = self.type_map.get(interface_name, None)
         if cached_type:
             self.validate_same_type_definition(interface_name, interface, cached_type)
@@ -450,7 +452,32 @@ def resolve_type(
                     # TODO: we should find the correct type here from the
                     # generic
                     if not type_definition.is_graphql_generic:
-                        return obj.__strawberry_definition__.name
+                        return type_definition.name
+
+                    # here we don't all the implementations of the generic
+                    # we need to find a way to find them, for now maybe
+                    # we can follow the union's approach and iterate over
+                    # all the types in the schema, but we should probably
+                    # optimize this
+
+                    return_type: Optional[GraphQLType] = None
+
+                    for possible_concrete_type in self.type_map.values():
+                        possible_type = possible_concrete_type.definition
+
+                        if not isinstance(
+                            possible_type, StrawberryObjectDefinition
+                        ):  # pragma: no cover
+                            continue
+
+                        if possible_type.is_implemented_by(obj):
+                            return_type = possible_concrete_type.implementation
+                            break
+
+                    if return_type:
+                        assert isinstance(return_type, GraphQLNamedType)
+
+                        return return_type.name
 
                 # Revert to calling is_type_of for cases where a direct subclass
                 # of the interface is not returned (i.e. an ORM object)
@@ -794,7 +821,7 @@ def from_union(self, union: StrawberryUnion) -> GraphQLUnionType:
             if not StrawberryUnion.is_valid_union_type(type_):
                 raise InvalidUnionTypeError(union_name, type_, union_definition=union)
 
-        # Don't reevaluate known types
+        # Don't re-evaluate known types
         if union_name in self.type_map:
             graphql_union = self.type_map[union_name].implementation
             assert isinstance(graphql_union, GraphQLUnionType)  # For mypy
diff --git a/strawberry/types/types.py b/strawberry/types/types.py
--- a/strawberry/types/types.py
+++ b/strawberry/types/types.py
@@ -17,6 +17,7 @@
 from typing_extensions import Self, deprecated
 
 from strawberry.type import (
+    StrawberryList,
     StrawberryType,
     StrawberryTypeVar,
     WithStrawberryObjectDefinition,
@@ -169,21 +170,43 @@ def is_implemented_by(self, root: Type[WithStrawberryObjectDefinition]) -> bool:
             return False
 
         # Check the mapping of all fields' TypeVars
-        for generic_field in type_definition.fields:
-            generic_field_type = generic_field.type
-            if not isinstance(generic_field_type, StrawberryTypeVar):
+        for field in type_definition.fields:
+            if not field.is_graphql_generic:
+                continue
+
+            value = getattr(root, field.name)
+            generic_field_type = field.type
+
+            while isinstance(generic_field_type, StrawberryList):
+                generic_field_type = generic_field_type.of_type
+
+                assert isinstance(value, (list, tuple))
+
+                if len(value) == 0:
+                    # We can't infer the type of an empty list, so we just
+                    # return the first one we find
+                    return True
+
+                value = value[0]
+
+            if isinstance(generic_field_type, StrawberryTypeVar):
+                type_var = generic_field_type.type_var
+            # TODO: I don't think we support nested types properly
+            # if there's a union that has two nested types that
+            # are have the same field with different types, we might
+            # not be able to differentiate them
+            else:
                 continue
 
             # For each TypeVar found, get the expected type from the copy's type map
-            expected_concrete_type = self.type_var_map.get(
-                generic_field_type.type_var.__name__
-            )
+            expected_concrete_type = self.type_var_map.get(type_var.__name__)
+
+            # this shouldn't happen, but we do a defensive check just in case
             if expected_concrete_type is None:
-                # TODO: Should this return False?
                 continue
 
             # Check if the expected type matches the type found on the type_map
-            real_concrete_type = type(getattr(root, generic_field.name))
+            real_concrete_type = type(value)
 
             # TODO: uniform type var map, at the moment we map object types
             # to their class (not to TypeDefinition) while we map enum to
diff --git a/strawberry/union.py b/strawberry/union.py
--- a/strawberry/union.py
+++ b/strawberry/union.py
@@ -194,7 +194,6 @@ def _resolve_union_type(
             # Union in case a nested generic object matches against more than one type.
             concrete_types_for_union = (type_map[x.name] for x in type_.types)
 
-            # TODO: do we still need to iterate over all types in `type_map`?
             for possible_concrete_type in chain(
                 concrete_types_for_union, type_map.values()
             ):
@@ -213,13 +212,9 @@ def _resolve_union_type(
                     info.field_name, str(type(root)), set(type_.types)
                 )
 
-            # Return the name of the type. Returning the actual type is now deprecated
-            if isinstance(return_type, GraphQLNamedType):
-                # TODO: Can return_type ever _not_ be a GraphQLNamedType?
-                return return_type.name
-            else:
-                # TODO: check if this is correct
-                return return_type.__name__  # type: ignore
+            assert isinstance(return_type, GraphQLNamedType)
+
+            return return_type.name
 
         return _resolve_union_type
 


Test patch:
diff --git a/tests/schema/test_generics.py b/tests/schema/test_generics.py
--- a/tests/schema/test_generics.py
+++ b/tests/schema/test_generics.py
@@ -3,8 +3,6 @@
 from typing import Any, Generic, List, Optional, TypeVar, Union
 from typing_extensions import Self
 
-import pytest
-
 import strawberry
 
 
@@ -284,52 +282,6 @@ def multiple(self) -> Multiple[int, str]:
     }
 
 
-def test_support_nested_generics():
-    T = TypeVar("T")
-
-    @strawberry.type
-    class User:
-        name: str
-
-    @strawberry.type
-    class Edge(Generic[T]):
-        node: T
-
-    @strawberry.type
-    class Connection(Generic[T]):
-        edge: Edge[T]
-
-    @strawberry.type
-    class Query:
-        @strawberry.field
-        def users(self) -> Connection[User]:
-            return Connection(edge=Edge(node=User(name="Patrick")))
-
-    schema = strawberry.Schema(query=Query)
-
-    query = """{
-        users {
-            __typename
-            edge {
-                __typename
-                node {
-                    name
-                }
-            }
-        }
-    }"""
-
-    result = schema.execute_sync(query)
-
-    assert not result.errors
-    assert result.data == {
-        "users": {
-            "__typename": "UserConnection",
-            "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}},
-        }
-    }
-
-
 def test_supports_optional():
     T = TypeVar("T")
 
@@ -645,58 +597,6 @@ def example(self) -> Union[Fallback, Edge[int, str]]:
     }
 
 
-def test_supports_generic_in_unions_with_nesting():
-    T = TypeVar("T")
-
-    @strawberry.type
-    class User:
-        name: str
-
-    @strawberry.type
-    class Edge(Generic[T]):
-        node: T
-
-    @strawberry.type
-    class Connection(Generic[T]):
-        edge: Edge[T]
-
-    @strawberry.type
-    class Fallback:
-        node: str
-
-    @strawberry.type
-    class Query:
-        @strawberry.field
-        def users(self) -> Union[Connection[User], Fallback]:
-            return Connection(edge=Edge(node=User(name="Patrick")))
-
-    schema = strawberry.Schema(query=Query)
-
-    query = """{
-        users {
-            __typename
-            ... on UserConnection {
-                edge {
-                    __typename
-                    node {
-                        name
-                    }
-                }
-            }
-        }
-    }"""
-
-    result = schema.execute_sync(query)
-
-    assert not result.errors
-    assert result.data == {
-        "users": {
-            "__typename": "UserConnection",
-            "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}},
-        }
-    }
-
-
 def test_supports_multiple_generics_in_union():
     T = TypeVar("T")
 
@@ -937,7 +837,6 @@ def user(self) -> Union[User, Edge[User]]:
     assert result.data == {"user": {"__typename": "UserEdge", "nodes": []}}
 
 
-@pytest.mark.xfail()
 def test_raises_error_when_unable_to_find_type():
     T = TypeVar("T")
 
@@ -971,10 +870,9 @@ def user(self) -> Union[User, Edge[User]]:
 
     result = schema.execute_sync(query)
 
-    assert result.errors[0].message == (
-        "Unable to find type for <class 'tests.schema.test_generics."
-        "test_raises_error_when_unable_to_find_type.<locals>.Edge'> "
-        "and (<class 'str'>,)"
+    assert (
+        'of the field "user" is not in the list of the types of the union'
+        in result.errors[0].message
     )
 
 
diff --git a/tests/schema/test_generics_nested.py b/tests/schema/test_generics_nested.py
new file mode 100644
--- /dev/null
+++ b/tests/schema/test_generics_nested.py
@@ -0,0 +1,365 @@
+import textwrap
+from typing import Generic, List, Optional, TypeVar, Union
+
+import strawberry
+from strawberry.scalars import JSON
+
+
+def test_support_nested_generics():
+    T = TypeVar("T")
+
+    @strawberry.type
+    class User:
+        name: str
+
+    @strawberry.type
+    class Edge(Generic[T]):
+        node: T
+
+    @strawberry.type
+    class Connection(Generic[T]):
+        edge: Edge[T]
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def users(self) -> Connection[User]:
+            return Connection(edge=Edge(node=User(name="Patrick")))
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """{
+        users {
+            __typename
+            edge {
+                __typename
+                node {
+                    name
+                }
+            }
+        }
+    }"""
+
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert result.data == {
+        "users": {
+            "__typename": "UserConnection",
+            "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}},
+        }
+    }
+
+
+def test_unions_nested_inside_a_list():
+    T = TypeVar("T")
+
+    @strawberry.type
+    class JsonBlock:
+        data: JSON
+
+    @strawberry.type
+    class BlockRowType(Generic[T]):
+        total: int
+        items: List[T]
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def blocks(
+            self,
+        ) -> List[Union[BlockRowType[int], BlockRowType[str], JsonBlock]]:
+            return [
+                BlockRowType(total=3, items=["a", "b", "c"]),
+                BlockRowType(total=1, items=[1, 2, 3, 4]),
+                JsonBlock(data=JSON({"a": 1})),
+            ]
+
+    schema = strawberry.Schema(query=Query)
+
+    result = schema.execute_sync(
+        """query {
+        blocks {
+            __typename
+            ... on IntBlockRowType {
+                a: items
+            }
+            ... on StrBlockRowType {
+                b: items
+            }
+            ... on JsonBlock {
+                data
+            }
+        }
+    }"""
+    )
+
+    assert not result.errors
+
+    assert result.data == {
+        "blocks": [
+            {"__typename": "StrBlockRowType", "b": ["a", "b", "c"]},
+            {"__typename": "IntBlockRowType", "a": [1, 2, 3, 4]},
+            {"__typename": "JsonBlock", "data": {"a": 1}},
+        ]
+    }
+
+
+def test_unions_nested_inside_a_list_with_no_items():
+    T = TypeVar("T")
+
+    @strawberry.type
+    class JsonBlock:
+        data: JSON
+
+    @strawberry.type
+    class BlockRowType(Generic[T]):
+        total: int
+        items: List[T]
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def blocks(
+            self,
+        ) -> List[Union[BlockRowType[int], BlockRowType[str], JsonBlock]]:
+            return [
+                BlockRowType(total=3, items=[]),
+                BlockRowType(total=1, items=[]),
+                JsonBlock(data=JSON({"a": 1})),
+            ]
+
+    schema = strawberry.Schema(query=Query)
+
+    result = schema.execute_sync(
+        """query {
+        blocks {
+            __typename
+            ... on IntBlockRowType {
+                a: items
+            }
+            ... on StrBlockRowType {
+                b: items
+            }
+            ... on JsonBlock {
+                data
+            }
+        }
+    }"""
+    )
+
+    assert not result.errors
+
+    assert result.data == {
+        "blocks": [
+            {"__typename": "IntBlockRowType", "a": []},
+            {"__typename": "IntBlockRowType", "a": []},
+            {"__typename": "JsonBlock", "data": {"a": 1}},
+        ]
+    }
+
+
+def test_unions_nested_inside_a_list_of_lists():
+    T = TypeVar("T")
+
+    @strawberry.type
+    class JsonBlock:
+        data: JSON
+
+    @strawberry.type
+    class BlockRowType(Generic[T]):
+        total: int
+        items: List[List[T]]
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def blocks(
+            self,
+        ) -> List[Union[BlockRowType[int], BlockRowType[str], JsonBlock]]:
+            return [
+                BlockRowType(total=3, items=[["a", "b", "c"]]),
+                BlockRowType(total=1, items=[[1, 2, 3, 4]]),
+                JsonBlock(data=JSON({"a": 1})),
+            ]
+
+    schema = strawberry.Schema(query=Query)
+
+    result = schema.execute_sync(
+        """query {
+        blocks {
+            __typename
+            ... on IntBlockRowType {
+                a: items
+            }
+            ... on StrBlockRowType {
+                b: items
+            }
+            ... on JsonBlock {
+                data
+            }
+        }
+    }"""
+    )
+
+    assert not result.errors
+
+    assert result.data == {
+        "blocks": [
+            {"__typename": "StrBlockRowType", "b": [["a", "b", "c"]]},
+            {"__typename": "IntBlockRowType", "a": [[1, 2, 3, 4]]},
+            {"__typename": "JsonBlock", "data": {"a": 1}},
+        ]
+    }
+
+
+def test_using_generics_with_an_interface():
+    T = TypeVar("T")
+
+    @strawberry.interface
+    class BlockInterface:
+        id: strawberry.ID
+        disclaimer: Optional[str] = strawberry.field(default=None)
+
+    @strawberry.type
+    class JsonBlock(BlockInterface):
+        data: JSON
+
+    @strawberry.type
+    class BlockRowType(BlockInterface, Generic[T]):
+        total: int
+        items: List[T]
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def blocks(self) -> List[BlockInterface]:
+            return [
+                BlockRowType(id=strawberry.ID("3"), total=3, items=["a", "b", "c"]),
+                BlockRowType(id=strawberry.ID("1"), total=1, items=[1, 2, 3, 4]),
+                JsonBlock(id=strawberry.ID("2"), data=JSON({"a": 1})),
+            ]
+
+    schema = strawberry.Schema(
+        query=Query, types=[BlockRowType[int], JsonBlock, BlockRowType[str]]
+    )
+
+    expected_schema = textwrap.dedent(
+        '''
+        interface BlockInterface {
+          id: ID!
+          disclaimer: String
+        }
+
+        type IntBlockRowType implements BlockInterface {
+          id: ID!
+          disclaimer: String
+          total: Int!
+          items: [Int!]!
+        }
+
+        """
+        The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
+        """
+        scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")
+
+        type JsonBlock implements BlockInterface {
+          id: ID!
+          disclaimer: String
+          data: JSON!
+        }
+
+        type Query {
+          blocks: [BlockInterface!]!
+        }
+
+        type StrBlockRowType implements BlockInterface {
+          id: ID!
+          disclaimer: String
+          total: Int!
+          items: [String!]!
+        }
+    '''
+    ).strip()
+
+    assert str(schema) == expected_schema
+
+    result = schema.execute_sync(
+        """query {
+        blocks {
+            id
+            __typename
+            ... on IntBlockRowType {
+                a: items
+            }
+            ... on StrBlockRowType {
+                b: items
+            }
+            ... on JsonBlock {
+                data
+            }
+        }
+    }"""
+    )
+
+    assert not result.errors
+
+    assert result.data == {
+        "blocks": [
+            {"id": "3", "__typename": "StrBlockRowType", "b": ["a", "b", "c"]},
+            {"id": "1", "__typename": "IntBlockRowType", "a": [1, 2, 3, 4]},
+            {"id": "2", "__typename": "JsonBlock", "data": {"a": 1}},
+        ]
+    }
+
+
+def test_supports_generic_in_unions_with_nesting():
+    T = TypeVar("T")
+
+    @strawberry.type
+    class User:
+        name: str
+
+    @strawberry.type
+    class Edge(Generic[T]):
+        node: T
+
+    @strawberry.type
+    class Connection(Generic[T]):
+        edge: Edge[T]
+
+    @strawberry.type
+    class Fallback:
+        node: str
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def users(self) -> Union[Connection[User], Fallback]:
+            return Connection(edge=Edge(node=User(name="Patrick")))
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """{
+        users {
+            __typename
+            ... on UserConnection {
+                edge {
+                    __typename
+                    node {
+                        name
+                    }
+                }
+            }
+        }
+    }"""
+
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert result.data == {
+        "users": {
+            "__typename": "UserConnection",
+            "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}},
+        }
+    }