Skip to content

Fix double-wrapped array type for list arguments with a typed prepare method#2668

Merged
amomchilov merged 1 commit into
Shopify:mainfrom
dduugg:fix-graphql-mutation-prepare-list-double-wrap
Jul 11, 2026
Merged

Fix double-wrapped array type for list arguments with a typed prepare method#2668
amomchilov merged 1 commit into
Shopify:mainfrom
dduugg:fix-graphql-mutation-prepare-list-double-wrap

Conversation

@dduugg

@dduugg dduugg commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Motivation

Tapioca::Dsl::Compilers::GraphqlMutation (and GraphqlInputObject, which shares the same GraphqlTypeHelper.type_for helper) generate an incorrect, doubly-nested array type for a list-typed GraphQL argument whose prepare: method has a Sorbet sig.

Given:

class CreateComment < GraphQL::Schema::Mutation
  extend T::Sig

  class << self
    extend T::Sig
    sig { params(tags: T::Array[String], _context: T.untyped).returns(T::Array[String]) }
    def prepare_tags(tags, _context)
      tags.map(&:downcase)
    end
  end

  argument :tags, [String], "Tags for the comment", prepare: :prepare_tags

  def resolve(tags:)
    # ...
  end
end

Tapioca currently generates:

sig { params(tags: T::Array[T::Array[::String]]).returns(T.untyped) }

instead of the correct:

sig { params(tags: T::Array[::String]).returns(T.untyped) }

Root cause

In GraphqlTypeHelper#type_for, when a prepare_method has a valid sig, its return type unconditionally replaces parsed_type:

if prepare_method
  ...
  if valid_return_type?(prepare_return_type)
    parsed_type = prepare_return_type&.to_s
  end
end

if type.list?
  parsed_type = "T::Array[#{parsed_type}]"
end

A GraphQL prepare method receives (and returns) the argument's fully coerced value — including the list wrapper, when the argument type is a list. So when a prepare method's sig return type is used as parsed_type, it already reflects the full (possibly list) shape of the final value. The subsequent if type.list? block wraps it in T::Array[...] again, double-nesting it.

This bug forces users to strip Sorbet sigs from otherwise well-typed prepare methods just to get a correct RBI, which was the workaround applied in the wild that led me to find this (see Gusto/zenpayroll#356117).

Fix

Track whether parsed_type came from a prepare_method's return type, and skip the extra list-wrap in that case, since the prepare method's return type is already final.

Test plan

  • Added regression tests to graphql_mutation_spec.rb:
    • a list argument with a prepare: method whose sig returns the same list type (exercises the double-wrap bug directly)
    • a list argument with a prepare: method that has no valid return type (void/untyped), confirming the existing fallback list-wrap behavior is untouched by the fix
  • Added a regression test to graphql_input_object_spec.rb for a list argument with a prepare: method whose sig returns the same list type, since GraphqlInputObject shares the same GraphqlTypeHelper.type_for code path but previously had no prepare: coverage
  • Ran the full graphql_mutation_spec.rb and graphql_input_object_spec.rb suites — all pass (24 tests, 74 assertions)
  • Verified against the real-world case that surfaced this (a GraphQL mutation argument with a list-typed input object argument and a prepare: method with a matching T::Array[...] return sig) — regenerated RBI now produces T::Array[X] instead of T::Array[T::Array[X]], and downstream srb tc passes.

@dduugg dduugg requested a review from a team as a code owner July 10, 2026 17:23
… method

GraphqlMutation/GraphqlInputObject would generate T::Array[T::Array[X]]
for a list argument whose `prepare:` method has a sig, because the
prepare method's return type (already the fully-coerced list value)
was wrapped in T::Array[...] a second time.
@dduugg dduugg force-pushed the fix-graphql-mutation-prepare-list-double-wrap branch from 5be839d to 4c68de7 Compare July 10, 2026 18:20
@amomchilov amomchilov enabled auto-merge July 10, 2026 23:53
@amomchilov

Copy link
Copy Markdown
Contributor

Good find, thanks!

@amomchilov amomchilov merged commit 3c2bbb5 into Shopify:main Jul 11, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants