Repository: strawberry-graphql/strawberry

Issue:
Duck typing / being allow to override the field types 🦆
I'm opening this issue to document how we can make it easier to override return types.

## Problem description

When using a framework like Django, you tend to directly return models in resolvers, for example:

```python
@strawberry.type
class Query:
    @strawberry.field
    def all_users(self) -> list[UserType]:
         return models.User.objects.all()
```

Assuming that `UserType` has a subset of models.User fields, this will work at runtime as we don't do a type check of the returned type[1].

Unfortunately this break type checking (mypy, pyright, etc) as the return type of `models.User.objects.all()` is `Queryset[models.User]` and the return type of `all_users` is `list[UserType]`.

## Ideas 

One way to work around this is the following:

```python
@strawberry.type
class Query:
    @strawberry.field
    def all_users(self) -> list[UserType]:
         return cast(list[UserType], models.User.objects.all())
```

---

@jkimbo suggested to allow doing something like this:

```python
@strawberry.type
class Query:
    @strawberry.field(field_type=list[UserType])
    def all_users(self) -> Queryset[models.User]:
        return models.User.objects.all()
```

and 

```python
@strawberry.type
class MyType:
  my_field: float = strawberry.field(field_type=str)

MyType(my_field=1.33) # this wouldn’t be a problem
```


[1] graphql-core has some type checks, but it is for scalars and nullability.



Discussion:
(it was actually @aryaniyaps that suggested it first here: https://discord.com/channels/689806334337482765/741575032257511485/1027417798839914546 )

Also I updated your description @patrick91 to make the suggested alternative analogous to the example problem.

Resolving patch:
diff --git a/strawberry/federation/field.py b/strawberry/federation/field.py
--- a/strawberry/federation/field.py
+++ b/strawberry/federation/field.py
@@ -42,6 +42,7 @@ def field(
     default: Any = UNSET,
     default_factory: Union[Callable[..., object], object] = UNSET,
     directives: Sequence[object] = (),
+    graphql_type: Optional[Any] = None,
 ) -> T:
     ...
 
@@ -65,6 +66,7 @@ def field(
     default: Any = UNSET,
     default_factory: Union[Callable[..., object], object] = UNSET,
     directives: Sequence[object] = (),
+    graphql_type: Optional[Any] = None,
 ) -> Any:
     ...
 
@@ -88,6 +90,7 @@ def field(
     default: Any = UNSET,
     default_factory: Union[Callable[..., object], object] = UNSET,
     directives: Sequence[object] = (),
+    graphql_type: Optional[Any] = None,
 ) -> StrawberryField:
     ...
 
@@ -110,6 +113,7 @@ def field(
     default: Any = dataclasses.MISSING,
     default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
     directives: Sequence[object] = (),
+    graphql_type: Optional[Any] = None,
     # This init parameter is used by PyRight to determine whether this field
     # is added in the constructor or not. It is not used to change
     # any behavior at the moment.
@@ -159,4 +163,5 @@ def field(
         default_factory=default_factory,
         init=init,  # type: ignore
         directives=directives,
+        graphql_type=graphql_type,
     )
diff --git a/strawberry/field.py b/strawberry/field.py
--- a/strawberry/field.py
+++ b/strawberry/field.py
@@ -350,6 +350,7 @@ def field(
     default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
     metadata: Optional[Mapping[Any, Any]] = None,
     directives: Optional[Sequence[object]] = (),
+    graphql_type: Optional[Any] = None,
 ) -> T:
     ...
 
@@ -367,6 +368,7 @@ def field(
     default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
     metadata: Optional[Mapping[Any, Any]] = None,
     directives: Optional[Sequence[object]] = (),
+    graphql_type: Optional[Any] = None,
 ) -> Any:
     ...
 
@@ -384,6 +386,7 @@ def field(
     default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
     metadata: Optional[Mapping[Any, Any]] = None,
     directives: Optional[Sequence[object]] = (),
+    graphql_type: Optional[Any] = None,
 ) -> StrawberryField:
     ...
 
@@ -400,6 +403,7 @@ def field(
     default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
     metadata: Optional[Mapping[Any, Any]] = None,
     directives: Optional[Sequence[object]] = (),
+    graphql_type: Optional[Any] = None,
     # This init parameter is used by PyRight to determine whether this field
     # is added in the constructor or not. It is not used to change
     # any behavior at the moment.
@@ -420,10 +424,12 @@ def field(
     it can be used both as decorator and as a normal function.
     """
 
+    type_annotation = StrawberryAnnotation.from_annotation(graphql_type)
+
     field_ = StrawberryField(
         python_name=None,
         graphql_name=name,
-        type_annotation=None,
+        type_annotation=type_annotation,
         description=description,
         is_subscription=is_subscription,
         permission_classes=permission_classes or [],
diff --git a/strawberry/object_type.py b/strawberry/object_type.py
--- a/strawberry/object_type.py
+++ b/strawberry/object_type.py
@@ -66,6 +66,12 @@ def _check_field_annotations(cls: Type):
         # to make sure dataclasses.dataclass is ready for it
         if isinstance(field_, StrawberryField):
 
+            # If the field has a type override then use that instead of using
+            # the class annotations or resolver annotation
+            if field_.type_annotation is not None:
+                cls_annotations[field_name] = field_.type_annotation.annotation
+                continue
+
             # Make sure the cls has an annotation
             if field_name not in cls_annotations:
                 # If the field uses the default resolver, the field _must_ be


Test patch:
diff --git a/tests/schema/test_fields.py b/tests/schema/test_fields.py
--- a/tests/schema/test_fields.py
+++ b/tests/schema/test_fields.py
@@ -113,3 +113,37 @@ class Query:
     assert result.data == {
         "a": 1.33,
     }
+
+
+def test_field_type_override():
+    @strawberry.type
+    class Query:
+        a: float = strawberry.field(graphql_type=str)
+        b = strawberry.field(graphql_type=int)
+
+        @strawberry.field(graphql_type=float)
+        def c(self):
+            return "3.4"
+
+    schema = strawberry.Schema(Query)
+
+    expected = """
+    type Query {
+      a: String!
+      b: Int!
+      c: Float!
+    }
+    """
+
+    assert print_schema(schema) == textwrap.dedent(expected).strip()
+
+    query = "{ a, b, c }"
+
+    result = schema.execute_sync(query, root_value=Query(a=1.33, b=2))
+
+    assert not result.errors
+    assert result.data == {
+        "a": "1.33",
+        "b": 2,
+        "c": 3.4,
+    }