88from functools import cached_property
99from typing import (
1010 TYPE_CHECKING ,
11+ Annotated ,
1112 Any ,
1213 NoReturn ,
1314 TypeAlias ,
1415 TypeVar ,
1516 Union ,
17+ get_args ,
18+ get_origin ,
1619 overload ,
1720)
1821
1922from strawberry .annotation import StrawberryAnnotation
20- from strawberry .exceptions import InvalidArgumentTypeError , InvalidDefaultFactoryError
23+ from strawberry .exceptions import (
24+ InvalidArgumentTypeError ,
25+ InvalidDefaultFactoryError ,
26+ InvalidStrawberryFieldAnnotationError ,
27+ )
2128from strawberry .types .base import (
2229 StrawberryType ,
2330 WithStrawberryObjectDefinition ,
@@ -124,6 +131,9 @@ def __init__( # noqa: PLR0917
124131 self .python_name = python_name
125132
126133 self .type_annotation = type_annotation
134+ # Cache the exact annotation object after successful validation and
135+ # resolution so replacing `type_annotation` automatically revalidates it.
136+ self ._validated_type_annotation : StrawberryAnnotation | None = None
127137
128138 self .description : str | None = description
129139 self .origin = origin
@@ -345,7 +355,14 @@ def resolve_type(
345355 with contextlib .suppress (NameError ):
346356 # Prioritise the field type over the resolver return type
347357 if self .type_annotation is not None :
348- resolved = self .type_annotation .resolve (type_definition = type_definition )
358+ type_annotation = self .type_annotation
359+ if type_annotation is not self ._validated_type_annotation :
360+ self ._validate_type_annotation (
361+ type_annotation ._evaluated_annotation
362+ )
363+
364+ resolved = type_annotation .resolve (type_definition = type_definition )
365+ self ._validated_type_annotation = type_annotation
349366 elif self .base_resolver is not None and self .base_resolver .type is not None :
350367 # Handle unannotated functions (such as lambdas)
351368 # Generics will raise MissingTypesForGenericError later
@@ -355,6 +372,17 @@ def resolve_type(
355372
356373 return resolved
357374
375+ def _validate_type_annotation (self , annotation : object ) -> None :
376+ if (
377+ self .python_name is not None
378+ and isinstance (self .origin , type )
379+ and _contains_strawberry_field (annotation )
380+ ):
381+ raise InvalidStrawberryFieldAnnotationError (
382+ field_name = self .python_name ,
383+ cls = self .origin ,
384+ )
385+
358386 def copy_with (
359387 self , type_var_map : Mapping [str , StrawberryType | builtins .type ]
360388 ) -> Self :
@@ -395,6 +423,29 @@ def is_async(self) -> bool:
395423 return self ._has_async_base_resolver
396424
397425
426+ def _contains_strawberry_field (
427+ annotation : object ,
428+ * ,
429+ at_field_annotation_root : bool = True ,
430+ ) -> bool :
431+ if get_origin (annotation ) is Annotated :
432+ annotation , * metadata = get_args (annotation )
433+ if not at_field_annotation_root and any (
434+ isinstance (item , StrawberryField ) for item in metadata
435+ ):
436+ return True
437+
438+ return _contains_strawberry_field (
439+ annotation ,
440+ at_field_annotation_root = at_field_annotation_root ,
441+ )
442+
443+ return any (
444+ _contains_strawberry_field (arg , at_field_annotation_root = False )
445+ for arg in get_args (annotation )
446+ )
447+
448+
398449# NOTE: we are separating the sync and async resolvers because using both
399450# in the same function will cause mypy to raise an error. Not sure if it is a bug
400451
0 commit comments