Skip to main content
Unions allow you to group different object types into a single type. They’re perfect for fields that can return one of several possible object types, like polymorphic associations or search results.

Basic Union Definition

Define unions in files or inline within your schema:
# app/graphql/unions/person.rb
module GraphQL
  class Person < GraphQL::Union
    append User, Admin
  end
end
The resulting GraphQL type:
union Person = User | Admin
The shorthand syntax is preferred - it’s concise and avoids referencing types by their classes.

Adding Members

Use the append method to add object types to the union:
union 'SearchResult' do
  append :post, :comment, :user
end

Validation Rules

Unions enforce strict validation:
  1. Minimum members: Must contain at least one member
  2. Same base class: All members must share the same base class
  3. Object types only: Members must be GraphQL::Object types
# ✓ Valid
union 'SearchResult' do
  append :post, :comment  # Both are objects
end

# ✗ Invalid - different base classes
union 'Invalid' do
  append :user, :post_input  # Object + Input type
end

# ✗ Invalid - no members
union 'Empty' do
  # Raises: "A union must contain at least one member"
end

Type Resolution

Unions require type resolution to determine which member type represents a given value:

Default Resolution

The default type_for method checks each member type:
union 'SearchResult' do
  append :post, :comment, :user
  
  # Default implementation:
  def self.type_for(value, request)
    all_members&.reverse_each&.find { |t| t.valid_member?(value) }
  end
end

Custom Resolution

Override for specific logic, like polymorphic associations:
union 'Commentable' do
  append :post, :video
  
  def self.type_for(value, request)
    # Use Rails polymorphic type column
    request.find_type(value.commentable_type)
  end
end
Always use request.find_type for type lookups - it’s cached during the request and respects namespaces.

Key Differences from Interfaces

Unions and interfaces both enable polymorphism but serve different purposes:

Unions

  • No shared fields
  • Group unrelated types
  • Pure composition
  • Perfect for polymorphic belongs_to

Interfaces

  • Define shared fields
  • Objects inherit fields
  • Shared resolution logic
  • Perfect for STI models
Unions cannot have fields. To access any data, you must use spreads to query type-specific fields.

Usage in Queries

Unions require spreads to access fields since they don’t define any themselves:
query_fields do
  field :search, 'SearchResult', array: true do
    argument :query, :string, null: false
  end
end

def search(query:)
  # Returns mixed array of Posts, Comments, and Users
  GlobalSearch.perform(query)
end
Query with Spreads
query {
  search(query: "graphql") {
    __typename
    
    ... on Post {
      id
      title
      body
    }
    
    ... on Comment {
      id
      text
      author {
        name
      }
    }
    
    ... on User {
      id
      email
      username
    }
  }
}
The __typename field is the only field you can query directly on a union. Use spreads for everything else.

Common Patterns

Polymorphic Associations

Polymorphic Belongs To
# Rails model
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

# GraphQL union
union 'Commentable', of_types: %w[Post Video]

object 'Comment' do
  field :id
  field :text
  field :commentable, 'Commentable'
end

Search Results

Multiple Content Types
union 'SearchResult', of_types: %w[Post Comment User Tag]

query_fields do
  field :search, 'SearchResult', array: true do
    argument :query, :string, null: false
    argument :types, :string, array: true
  end
end

def search(query:, types: nil)
  results = []
  types ||= %w[Post Comment User Tag]
  
  results += Post.search(query) if types.include?('Post')
  results += Comment.search(query) if types.include?('Comment')
  results += User.search(query) if types.include?('User')
  results += Tag.search(query) if types.include?('Tag')
  
  results
end

Activity Feeds

Different Event Types
union 'ActivityEvent', of_types: %w[
  CommentCreated
  PostPublished
  UserFollowed
  PostLiked
]

query_fields do
  field :activity_feed, 'ActivityEvent', array: true do
    argument :user_id, :id, null: false
  end
end

Error Handling

Success or Error
union 'CreateUserResult', of_types: %w[User ValidationError]

mutation_fields do
  field :create_user, 'CreateUserResult', null: false do
    argument :input, 'UserInput', null: false
  end
end

def create_user(input:)
  user = User.new(input.params)
  if user.save
    user
  else
    ValidationError.new(errors: user.errors)
  end
end

Descriptions

Document unions for API clarity:
union 'SearchResult' do
  desc 'Possible search result types'
  append :post, :comment, :user
end

Inspecting Unions

Unions provide helpful inspection output:
GraphQL::SearchResult.inspect
# => #<GraphQL::Union SearchResult (3) {Post | Comment | User}>

Best Practices

Unions are perfect for Rails polymorphic associations. Map the polymorphic belongs_to to a union of possible types.
When to use unions:
  • Polymorphic belongs_to associations
  • Search results across multiple types
  • Activity feeds with different event types
  • Result types (success/error patterns)
  • Any field that returns “one of several types”
Avoid using unions when:
  • Types share common fields (use interfaces instead)
  • You need to query shared fields without spreads
  • All types inherit from a common base (use interfaces)

Member Access

Access union members programmatically:
GraphQL::SearchResult.all_members
# => [GraphQL::Post, GraphQL::Comment, GraphQL::User]

GraphQL::SearchResult.members.size
# => 3

GraphQL::SearchResult.of_kind
# => GraphQL::Object

Build docs developers (and LLMs) love