strawberry-graphql/strawberry

buggy generic interfaces

Closed this issue · 3 comments

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

Upvote & Fund

  • We're using Polar.sh 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.
Fund with Polar

I guess the problem is in this line

The outer Type (which implements the interface) is checked but not the nested values in items

image

But the outer loop here 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:

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:

def get_type_resolver(self, type_map: TypeMap) -> GraphQLTypeResolver:
def _resolve_union_type(
root: Any, info: GraphQLResolveInfo, type_: GraphQLAbstractType
) -> str:
assert isinstance(type_, GraphQLUnionType)
from strawberry.types.types import StrawberryObjectDefinition
# If the type given is not an Object type, try resolving using `is_type_of`
# defined on the union's inner types
if not has_object_definition(root):
for inner_type in type_.types:
if inner_type.is_type_of is not None and inner_type.is_type_of(
root, info
):
return inner_type.name
# Couldn't resolve using `is_type_of`
raise WrongReturnTypeForUnion(info.field_name, str(type(root)))
return_type: Optional[GraphQLType]
# Iterate over all of our known types and find the first concrete
# type that implements the type. We prioritise checking types named in the
# 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()
):
possible_type = possible_concrete_type.definition
if not isinstance(possible_type, StrawberryObjectDefinition):
continue
if possible_type.is_implemented_by(root):
return_type = possible_concrete_type.implementation
break
else:
return_type = None
# Make sure the found type is expected by the Union
if return_type is None or return_type not in type_.types:
raise UnallowedReturnTypeForUnion(
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
return _resolve_union_type

which is separated from the interface part, so I need to think how we could combine both