Repository: strawberry-graphql/strawberry

Issue:
StrawberryUnion's type resolver does not prioritise types in the Union
## Describe the Bug

StrawberryUnion's type resolver iterates over all known types and returns the first type which successfully implements the returned object from the resolver.

Due to limitations of `TypeDefinition.is_implemented_by` when checking generic fields, this check might succeed prematurely for an object with the same top level structure.

If the wrong type is selected a `UnallowedReturnTypeForUnion` error is raised shortly afterwards.

Potentially it might be possible to improve `TypeDefinition.is_implemented_by` check for generic fields but that is beyond the scope of this issue.

This issue suggests a simple fix of first checking the types given in the union before falling back to checking all types. This might also be a small performance improvement if it is true that in most cases the correct type is listed in the Union.

Minimal reproduceable example: https://play.strawberry.rocks/?gist=f7d88898d127e65b12140fdd763f9ef2

## Existing Code
The below check is logically incorrect as `possible_type.is_implemented_by(root)` might be True for a similar object that is not the correct type.

```python
# Iterate over all of our known types and find the first concrete type that
# implements the type
for possible_concrete_type in type_map.values():
    possible_type = possible_concrete_type.definition
    if not isinstance(possible_type, TypeDefinition):
        continue
    if possible_type.is_implemented_by(root):
        return_type = possible_concrete_type.implementation
        break
else:
    return_type = None
```

## Example Fix
Here we check the types explicitly listed in the Union first, and then fallback to checking all types.

```python
# Iterate over the given types in the union and check if any provide a
# concrete type that implements the type.
for gql_object_type in type_.types:
    type_name = gql_object_type.name
    possible_concrete_type = type_map.get(type_name)
    if possible_concrete_type:
        possible_type = possible_concrete_type.definition
        if not isinstance(possible_type, TypeDefinition):
            continue
        if possible_type.is_implemented_by(root):
            return_type = possible_concrete_type.implementation
            break
else:
    return_type = None

# Otherwise, iterate over all of our known types and find the first concrete
# type that implements the type.
if return_type is None:
    for possible_concrete_type in type_map.values():
        possible_type = possible_concrete_type.definition
        if not isinstance(possible_type, TypeDefinition):
            continue
        if possible_type.is_implemented_by(root):
            return_type = possible_concrete_type.implementation
            break
```

## System Information

 - Operating system: N/A
 - Strawberry version (if applicable): 0.119.1



Discussion:

Resolving patch:
diff --git a/strawberry/union.py b/strawberry/union.py
--- a/strawberry/union.py
+++ b/strawberry/union.py
@@ -1,4 +1,5 @@
 import itertools
+from itertools import chain
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -152,14 +153,20 @@ def _resolve_union_type(
                     ):
                         return inner_type.name
 
-                # Couldn't resolve using `is_type_of``
+                # 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
-            for possible_concrete_type in type_map.values():
+            # 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, TypeDefinition):
                     continue


Test patch:
diff --git a/tests/schema/test_union.py b/tests/schema/test_union.py
--- a/tests/schema/test_union.py
+++ b/tests/schema/test_union.py
@@ -1,7 +1,7 @@
 import sys
 from dataclasses import dataclass
 from textwrap import dedent
-from typing import Optional, Union
+from typing import Generic, List, Optional, TypeVar, Union
 
 import pytest
 
@@ -187,6 +187,42 @@ def hello(self) -> Union[A, B]:
     )
 
 
+def test_unknown_types_are_rejected():
+    @strawberry.type
+    class Outside:
+        c: int
+
+    @strawberry.type
+    class A:
+        a: int
+
+    @strawberry.type
+    class B:
+        b: int
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def hello(self) -> Union[A, B]:
+            return Outside(c=5)  # type:ignore
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """
+    {
+        hello {
+            ... on A {
+                a
+            }
+        }
+    }
+    """
+
+    result = schema.execute_sync(query)
+
+    assert "Outside" in result.errors[0].message
+
+
 def test_named_union():
     @strawberry.type
     class A:
@@ -493,3 +529,76 @@ def user(self, data: Input) -> User:
                 return User(name=data.name, age=100)
 
         strawberry.Schema(query=Query)
+
+
+def test_union_with_similar_nested_generic_types():
+    """
+    Previously this failed due to an edge case where Strawberry would choose AContainer
+    as the resolved type for container_b due to the inability to exactly match the
+    nested generic `Container.items`.
+    """
+    T = TypeVar("T")
+
+    @strawberry.type
+    class Container(Generic[T]):
+        items: List[T]
+
+    @strawberry.type
+    class A:
+        a: str
+
+    @strawberry.type
+    class B:
+        b: int
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def container_a(self) -> Union[Container[A], A]:
+            return Container(items=[A("hello")])
+
+        @strawberry.field
+        def container_b(self) -> Union[Container[B], B]:
+            return Container(items=[B(3)])
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """
+     {
+        containerA {
+            __typename
+            ... on AContainer {
+                items {
+                    a
+                }
+            }
+            ... on A {
+                a
+            }
+        }
+    }
+    """
+
+    result = schema.execute_sync(query)
+
+    assert result.data["containerA"]["items"][0]["a"] == "hello"
+
+    query = """
+     {
+        containerB {
+            __typename
+            ... on BContainer {
+                items {
+                    b
+                }
+            }
+            ... on B {
+                b
+            }
+        }
+    }
+    """
+
+    result = schema.execute_sync(query)
+
+    assert result.data["containerB"]["items"][0]["b"] == 3