⚠ New Class-Based API ⚠

This describes a new API in v1.8.0. Check the upgrade notes for more info.

Extending the GraphQL-Ruby Type Definition System

While integrating GraphQL into your app, you can customize the definition DSL. For example, you might:

This guide describes various options for extending the class-based definition API. Keep in mind that these approaches may change as the API matures. If you’re having trouble, consider opening an issue on GitHub to get help.

Customization Overview

In general, the schema definition process goes like this:

This process will certainly change over time. The goal to entirely remove “legacy” GraphQL objects from the system. So, at that time, .to_graphql will no longer be used.

Another important note: after GraphQL-Ruby converts a class to a “legacy” object, the “legacy” object may be accessed using .graphql_definition. This cached instance is the “one true instance” used by GraphQL-Ruby.

Customizing type definitions

In your custom classes, you can override .to_graphql to customize the type that will be used at runtime. For example, to assign metadata values to an ObjectType:

class BaseObject < GraphQL::Schema::Object
  # Call this method in an Object class to set the permission level:
  def self.required_permission(permission_level)
    @required_permission = permission_level

  # This method is overridden to customize object types:
  def self.to_graphql
    type_defn = super # returns a GraphQL::ObjectType
    # Get a configured value and assign it to metadata
    type_defn.metadata[:required_permission] = @required_permission

# Then, in concrete classes
class Dossier < BaseObject
  # The Dossier object type will have `.metadata[:required_permission] # => :admin`
  required_permission :admin

Now, any runtime code which uses .metadata[:required_permission] will get the right value.

Customizing fields

Fields are generated in a different way. Instead of using classes, they are generated with instances of GraphQL::Schema::Field (or a subclass). In short, the definition process works like this:

# This is what happens under the hood, roughly:
# In an object class:
field :name, String, null: false
# ...
# Leads to:
field_config = GraphQL::Schema::Field.new(:name, String, null: false)
# Then, later:
field_config.to_graphql # => returns a GraphQL::Field instance

So, you can customize this process by:

For example, you can create a custom class which accepts a new parameter to initialize:

class AuthorizedField < GraphQL::Schema::Field
  # Override #initialize to take a new argument:
  def initialize(*args, required_permission:, **kwargs, &block)
    @required_permission = required_permission
    # Pass on the default args:
    super(*args, **kwargs, &block)

  def to_graphql
    field_defn = super # Returns a GraphQL::Field
    field_defn.metadata[:required_permission] = @required_permission

Then, pass the field class as field_class(...) wherever it should be used:

class BaseObject < GraphQL::Schema::Object
  # Use this class for defining fields
  field_class AuthorizedField

# And/Or
class BaseInterface < GraphQL::Schema::Interface
  field_class AuthorizedField

Now, AuthorizedField.new(*args, &block).to_graphql will be used to create GraphQL::Fields.

Customizing Arguments

Arguments may be customized in a similar way to Fields.

Then, in your custom argument class, you can use:

Customizing Enum Values

Enum values may be customized in a similar way to Fields.

Then, in your custom argument class, you can use:

Customization compatibility

Inevitably, this will result in some duplication while you migrate from one definition API to the other. Here are a couple of ways to re-use old customizations with the new framework:

Pass-through with accepts_definition. New schema classes have an accepts_definition method. They set up a configuration method which will pass the provided value to the existing (legacy-style) configuration function, for example:

# Given a legacy-style configuration function:
GraphQL::ObjectType.accepts_definitions({ permission_level: ->(...) { ... } })

# Prepare the config method in the base class:
class BaseObject < GraphQL::Schema::Object
  accepts_definition :permission_level

# Call the config method in the object class:
class Account < BaseObject
  permission_level 1

# Then, the runtime object will have the configured value, for example:
# => 1

See GraphQL::Schema::Member::AcceptsDefinition for the implementation.

Invoke .call directly. If you defined a module with a .call method, you can invoke that method during .to_graphql. For example:

class BaseObject < GraphQL::Schema::Object
  def self.to_graphql
    type_defn = super
    # Re-use the accepts_definition callback manually:
    DefinePermission.call(type_defn, required_permission: @required_permission)

Use .redefine. You can re-open a .define block at any time with .redefine. It returns a new, updated instance based on the old one. For example:

class BaseObject < GraphQL::Schema::Object
  def self.to_graphql
    type_defn = super
    # Read the value from the instance variable, since ivars don't work in `.define {...}` blocks
    configured_permission = @required_permission

    updated_type_defn = type_defn.redefine do
      # Use the accepts_definition method:

    # return the updated definition