Skip to content

Conversation

@patrick91
Copy link
Member

@patrick91 patrick91 commented Nov 29, 2025

Summary by Sourcery

Add support for configuring Strawberry fields via typing.Annotated metadata and surface a clear error when multiple field configurations are provided.

New Features:

  • Allow GraphQL fields on object and input types to be defined and configured using typing.Annotated with strawberry.field metadata.

Enhancements:

  • Preserve and interpret Annotated-based metadata for fields while correctly resolving their underlying Python and GraphQL types.

Documentation:

  • Document the Multiple Strawberry Fields error and how to resolve it when using Annotated with strawberry.field.

Tests:

  • Add comprehensive tests covering Annotated-based field definitions, including descriptions, GraphQL names, deprecation reasons, defaults, optionals, lists, nested types, metadata, schema generation, and interaction with regular fields.

Chores:

  • Introduce a MultipleStrawberryFieldsError exception for detecting and reporting multiple strawberry.field usages within a single Annotated field and export it from the exceptions package.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 29, 2025

Reviewer's Guide

Adds support for defining Strawberry GraphQL fields via typing.Annotated carrying strawberry.field metadata, updates the type resolver to interpret Annotated types (including validation for multiple strawberry.field annotations), wires a new MultipleStrawberryFieldsError into the exception system, and adds tests and docs around this behavior.

File-Level Changes

Change Details Files
Support strawberry.field metadata on typing.Annotated field definitions and integrate it into type resolution.
  • Extend the type resolver to detect Annotated field types, extract the underlying type and any embedded StrawberryField metadata, and construct StrawberryField instances accordingly.
  • Preserve non-StrawberryField Annotated metadata by passing the full Annotated type to StrawberryAnnotation when no StrawberryField is present.
  • Ensure default values defined via standard assignment still work with Annotated-based fields and that optional, list, nested, and input types are resolved correctly.
strawberry/types/type_resolver.py
Introduce and expose a dedicated error for multiple strawberry.field annotations in a single Annotated type and document its usage.
  • Add MultipleStrawberryFieldsError StrawberryException subclass that reports multiple strawberry.field usages on a single field and integrates with SourceFinder for error context.
  • Export MultipleStrawberryFieldsError from the public exceptions package and add user-facing documentation explaining when it is raised and how to resolve it.
strawberry/exceptions/multiple_strawberry_fields.py
strawberry/exceptions/__init__.py
docs/errors/multiple-strawberry-fields.md
Add tests validating Annotated-based field definitions and schema behavior.
  • Add tests covering descriptions, GraphQL names, deprecation reasons, default values, optional and list types, nested object types, custom metadata, interaction with regular and plain fields, schema generation, input types, and non-StrawberryField Annotated metadata.
  • Add a negative test ensuring that multiple strawberry.field annotations in a single Annotated field raise MultipleStrawberryFieldsError via the pytest.raises_strawberry_exception helper.
tests/types/test_object_types.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov
Copy link

codecov bot commented Nov 29, 2025

Codecov Report

❌ Patch coverage is 9.77011% with 157 lines in your changes missing coverage. Please review.
✅ Project coverage is 10.96%. Comparing base (c03892c) to head (39710e4).

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #4059       +/-   ##
===========================================
- Coverage   94.41%   10.96%   -83.45%     
===========================================
  Files         536      537        +1     
  Lines       35036    35208      +172     
  Branches     1842     1846        +4     
===========================================
- Hits        33079     3862    -29217     
- Misses       1659    31203    +29544     
+ Partials      298      143      -155     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@patrick91 patrick91 force-pushed the add-support-for-defining-fields-using-annotated branch from 4dd1427 to bbe863a Compare November 29, 2025 10:01
@patrick91 patrick91 marked this pull request as ready for review November 29, 2025 10:01
@patrick91 patrick91 requested a review from bellini666 November 29, 2025 10:01
@botberry
Copy link
Member

botberry commented Nov 29, 2025

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


This release adds support for defining fields using the Annotated syntax. This provides an
alternative way to specify field metadata alongside the type annotation.

Example usage:

from typing import Annotated

import strawberry


@strawberry.type
class Query:
    name: Annotated[str, strawberry.field(description="The name")]
    age: Annotated[int, strawberry.field(deprecation_reason="Use birthDate instead")]


@strawberry.input
class CreateUserInput:
    name: Annotated[str, strawberry.field(description="User's name")]
    email: Annotated[str, strawberry.field(description="User's email")]

This syntax works alongside the existing assignment syntax:

@strawberry.type
class Query:
    # Both styles work
    field1: Annotated[str, strawberry.field(description="Using Annotated")]
    field2: str = strawberry.field(description="Using assignment")

All strawberry.field() options are supported including description, name,
deprecation_reason, directives, metadata, and permission_classes.

Here's the tweet text:

🆕 Release (next) is out! Thanks to @patrick91 for the PR 👏

Get it here 👉 https://strawberry.rocks/release/(next)

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The new Annotated handling in type_resolver.py duplicates the StrawberryField construction logic in three branches; consider extracting a small helper (e.g. _build_strawberry_field(field_name, annotation, origin, module, default)) to reduce repetition and keep future changes to field creation centralized.
  • When a StrawberryField is found inside an Annotated type you currently drop all other Annotated metadata by using only the first argument as the annotation; if non-Strawberry metadata is expected to be preserved in this case as well, you may want to keep the full Annotated or otherwise explicitly handle or document the loss of the extra metadata.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `Annotated` handling in `type_resolver.py` duplicates the `StrawberryField` construction logic in three branches; consider extracting a small helper (e.g. `_build_strawberry_field(field_name, annotation, origin, module, default)`) to reduce repetition and keep future changes to field creation centralized.
- When a `StrawberryField` is found inside an `Annotated` type you currently drop all other `Annotated` metadata by using only the first argument as the annotation; if non-Strawberry metadata is expected to be preserved in this case as well, you may want to keep the full `Annotated` or otherwise explicitly handle or document the loss of the extra metadata.

## Individual Comments

### Comment 1
<location> `strawberry/types/type_resolver.py:153-162` </location>
<code_context>
+                for arg in rest:
</code_context>

<issue_to_address>
**issue (bug_risk):** Default value from the dataclass field is ignored when a StrawberryField is found in Annotated metadata.

When an `Annotated` type includes a `StrawberryField`, this branch assumes the default is already set on `strawberry_field_from_annotated`. That works only if the user passed `default=` to `strawberry.field()`, but not when the default is defined on the dataclass field itself:

```python
@strawberry.type
class A:
    foo: Annotated[int, strawberry.field()] = 5
    # or foo: Annotated[int, strawberry.field()] = dataclasses.field(default=5)
```

In these cases, the dataclass `Field` holds the default, so it gets dropped. Previously, `getattr(cls, field.name, dataclasses.MISSING)` preserved this. Please update the Annotated + `StrawberryField` path to also fall back to `getattr(cls, field.name, dataclasses.MISSING)` when the `StrawberryField` default is `dataclasses.MISSING`, so these defaults are not lost.
</issue_to_address>

### Comment 2
<location> `tests/types/test_object_types.py:239` </location>
<code_context>
     assert has_object_definition(JSON) is False
+
+
+def test_annotated_field_with_description():
+    @strawberry.type
+    class Query:
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test where `strawberry.field` is not the first metadata in `Annotated` but is still correctly picked up.

All existing `Annotated[...]` tests place `strawberry.field(...)` first in the metadata list, so they don’t verify that order truly doesn’t matter. Please add a case like:

```python
@strawberry.type
class Query:
    name: Annotated[str, "meta1", strawberry.field(description="desc"), "meta2"]

definition = get_object_definition(Query)
field = definition.fields[0]

assert field.python_name == "name"
assert field.description == "desc"
assert field.type is str
```

This guards against regressions where the implementation might accidentally depend on `strawberry.field` appearing in a specific position within the `Annotated` metadata.

Suggested implementation:

```python
def test_annotated_field_with_description():
    @strawberry.type
    class Query:
        name: Annotated[str, strawberry.field(description="The name")]

    definition = get_object_definition(Query)
    field = definition.fields[0]

    assert field.python_name == "name"
    assert field.description == "The name"
    assert field.type is str


def test_annotated_field_with_description_when_field_is_not_first_metadata():
    @strawberry.type
    class Query:
        name: Annotated[
            str,
            "meta1",
            strawberry.field(description="desc"),
            "meta2",
        ]

    definition = get_object_definition(Query)
    field = definition.fields[0]

    assert field.python_name == "name"
    assert field.description == "desc"
    assert field.type is str

```

Ensure that `Annotated` is imported in this test module, for example:

```python
from typing import Annotated
```

If this import already exists earlier in the file, no further changes are needed.
</issue_to_address>

### Comment 3
<location> `tests/types/test_object_types.py:293` </location>
<code_context>
+    assert field.type == Optional[str]
+
+
+def test_annotated_field_with_default_value():
+    @strawberry.type
+    class Query:
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for defaults/`default_factory` specified inside `strawberry.field(...)` when used in `Annotated`.

The current test only covers a default set via `= "default"` on an `Annotated` field. The new behavior should also support defaults coming from `strawberry.field(...)`, where the default is stored on the `StrawberryField` rather than the class attribute. Please add tests like:

```python
def test_annotated_field_with_default_in_metadata():
    @strawberry.type
    class Query:
        name: Annotated[
            str,
            strawberry.field(default="default", description="The name"),
        ]

    definition = get_object_definition(Query)
    field = definition.fields[0]

    assert field.default == "default"
    assert Query().name == "default"


def test_annotated_field_with_default_factory_in_metadata():
    @strawberry.type
    class Query:
        tags: Annotated[list[str], strawberry.field(default_factory=list)]

    assert Query().tags == []
```

These ensure `StrawberryField` defaults/default_factories are preserved when used via `Annotated`.
</issue_to_address>

### Comment 4
<location> `tests/types/test_object_types.py:416-417` </location>
<code_context>
+    assert "Use birthYear" in schema_str
+
+
+def test_annotated_field_with_input():
+    """Test that Annotated fields work correctly with input types."""
+
</code_context>

<issue_to_address>
**suggestion (testing):** Expand input-type tests to cover optional and defaulted `Annotated` fields.

Consider adding cases for:

- An optional input field, e.g. `name: Annotated[str | None, strawberry.field(description="...")]`, and asserting it is optional in the schema and functions correctly.
- A defaulted input field, e.g. `age: Annotated[int, strawberry.field(description="...", default=18)]`, and asserting the default appears both in the field definition and the GraphQL schema.

This would align input-type coverage with the existing object-type `Annotated` tests.

```suggestion
def test_annotated_field_with_input():
    """Test that Annotated fields work correctly with input types."""

    @strawberry.input
    class UserInput:
        name: Annotated[
            str | None,
            strawberry.field(description="The user name"),
        ]
        age: Annotated[
            int,
            strawberry.field(description="The user age", default=18),
        ]

    @strawberry.type
    class Query:
        @strawberry.field
        def get_user_age(self, input: UserInput) -> int:
            return input.age

    schema = strawberry.Schema(query=Query)
    schema_str = str(schema)

    # Optional field: should not be non-null in the schema
    assert "input UserInput" in schema_str
    assert '"""The user name"""' in schema_str
    assert 'name: String' in schema_str
    assert "name: String!" not in schema_str

    # Defaulted field: default should appear in the schema SDL
    assert '"""The user age"""' in schema_str
    assert "age: Int = 18" in schema_str

    # Executing without providing the optional field or the defaulted field value
    # should use the default for age and treat name as optional.
    result_with_defaults = schema.execute_sync(
        "{ getUserAge(input: {}) }",
    )
    assert result_with_defaults.errors is None
    assert result_with_defaults.data == {"getUserAge": 18}

    # Executing with an explicit value for age should override the default.
    result_with_value = schema.execute_sync(
        "{ getUserAge(input: { age: 30 }) }",
    )
    assert result_with_value.errors is None
    assert result_with_value.data == {"getUserAge": 30}
```
</issue_to_address>

### Comment 5
<location> `docs/errors/multiple-strawberry-fields.md:9` </location>
<code_context>
+
+## Description
+
+This error is thrown when using multiple `strawberry.field()` annotations inside
+an `Annotated` type. For example, the following code will throw this error:
+
</code_context>

<issue_to_address>
**suggestion (typo):** Consider using Python-idiomatic terminology by saying the error is "raised" instead of "thrown".

In Python, exceptions are typically described as being "raised" rather than "thrown". Consider updating the wording to: "This error is raised when using multiple `strawberry.field()` annotations inside..." to match common Python terminology.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 29, 2025

Greptile Overview

Greptile Summary

Added support for defining Strawberry GraphQL fields using Python's typing.Annotated syntax, allowing developers to specify field metadata inline with type annotations.

Key Changes:

  • Extended _get_fields() in type_resolver.py to detect and extract StrawberryField instances from Annotated type metadata
  • Added validation to prevent multiple strawberry.field() annotations on a single field, raising MultipleStrawberryFieldsError when detected
  • Created comprehensive test coverage including descriptions, GraphQL names, deprecation, defaults, lists, nested types, metadata, mixed field styles, schema generation, and input types
  • Added error documentation explaining the multiple fields restriction with clear examples

Implementation Details:
The feature works by parsing Annotated[Type, strawberry.field(...)] syntax during field resolution. When a StrawberryField is found in the Annotated metadata, it's extracted and configured with the actual type (first argument of Annotated) while preserving field configuration like description, GraphQL name, and deprecation reason.

Potential Concern:
Class-level default values (e.g., name: Annotated[str, strawberry.field(...)] = "default") may not be properly transferred from the dataclass field to the extracted StrawberryField. While this works for output types (where defaults aren't part of the GraphQL schema), it could cause issues for input types if defaults need to appear in the schema. Additional testing for input type defaults with Annotated syntax would verify this behavior.

Confidence Score: 4/5

  • This PR is safe to merge with one potential edge case to verify
  • Score reflects well-structured implementation with comprehensive tests and proper error handling. The code follows existing patterns in the codebase and includes documentation. One point deducted due to potential issue with class-level default values not being transferred to StrawberryField when extracted from Annotated metadata, which could affect input types.
  • Pay close attention to strawberry/types/type_resolver.py lines 162-173 regarding default value handling

Important Files Changed

File Analysis

Filename Score Overview
strawberry/types/type_resolver.py 4/5 Added support for extracting StrawberryField from Annotated type hints with validation for multiple fields; implementation handles type extraction and metadata but may need verification for class-level default value handling
strawberry/exceptions/multiple_strawberry_fields.py 5/5 New exception class following established patterns with proper error messages and source finding; well-implemented
tests/types/test_object_types.py 5/5 Comprehensive test coverage for Annotated field feature including descriptions, names, deprecation, defaults, lists, nested types, metadata, error cases, and schema generation

Sequence Diagram

sequenceDiagram
    participant User
    participant Decorator as @strawberry.type
    participant Dataclass as dataclasses.dataclass
    participant TypeResolver as _get_fields()
    participant AnnotatedParser as Annotated Handler
    participant Schema as GraphQL Schema

    User->>Decorator: Define class with Annotated field
    Note over User: name: Annotated[str, strawberry.field(...)]
    
    Decorator->>Dataclass: Wrap class as dataclass
    Dataclass->>Dataclass: Process fields and create __init__
    Note over Dataclass: Creates Field objects with defaults
    
    Decorator->>TypeResolver: Extract Strawberry fields
    TypeResolver->>TypeResolver: Iterate dataclasses.fields(cls)
    
    alt Field is StrawberryField
        TypeResolver->>TypeResolver: Validate and process existing field
    else Field is regular dataclass Field
        TypeResolver->>AnnotatedParser: Check if type is Annotated
        
        alt Type is Annotated with StrawberryField
            AnnotatedParser->>AnnotatedParser: Extract StrawberryField from metadata
            AnnotatedParser->>AnnotatedParser: Check for multiple fields (raises error)
            AnnotatedParser->>AnnotatedParser: Set python_name, type_annotation, origin
            AnnotatedParser->>TypeResolver: Return configured StrawberryField
        else Type is Annotated without StrawberryField
            AnnotatedParser->>TypeResolver: Create basic StrawberryField
        else Type is not Annotated
            TypeResolver->>TypeResolver: Create basic StrawberryField
        end
    end
    
    TypeResolver->>Schema: Return list of StrawberryFields
    Schema->>Schema: Build GraphQL schema definition
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 29, 2025

CodSpeed Performance Report

Merging #4059 will not alter performance

Comparing add-support-for-defining-fields-using-annotated (39710e4) with main (c03892c)

Summary

✅ 28 untouched

- Fix terminology: use "raised" instead of "thrown" in docs
- Transfer default values from dataclass field to StrawberryField for Annotated syntax
- Add test for input type defaults appearing in GraphQL schema
field_type = field.type
strawberry_field_from_annotated: StrawberryField | None = None

if get_origin(field_type) is Annotated:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: maybe instead of this, implement a separate function, like _get_field_from_annotated(), which receives the field, does the validation we have internally here, and then we have a single place creating a StrawberryField instead of 2

patrick91 and others added 2 commits December 2, 2025 17:41
Co-authored-by: Thiago Bellini Ribeiro <thiago@bellini.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants