Custom scalar in GraphQL-ruby
GraphQL-ruby provides built-in scalar types like String, Int, Float, Boolean, and ID. However, sometimes you need custom scalar types to handle specific data formats like dates, JSON, or custom identifiers.
Why Custom Scalars?
Custom scalars are useful when you need to:
- Handle dates and times in a specific format
- Work with JSON data
- Validate custom data formats
- Transform data between client and server
Creating a Custom Scalar
Date Scalar Example
Let's create a custom Date scalar:
# app/graphql/types/date_type.rb
module Types
class DateType < Types::BaseScalar
description "A date value"
def self.coerce_input(value, context)
Date.parse(value)
rescue ArgumentError
raise GraphQL::CoercionError, "cannot coerce #{value.inspect} to Date"
end
def self.coerce_result(value, context)
value.iso8601
end
end
end
Usage
Use your custom scalar in types and mutations:
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :birthday, Types::DateType, null: true
end
end
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
argument :name, String, required: true
argument :birthday, Types::DateType, required: false
field :user, Types::UserType, null: true
def resolve(name:, birthday: nil)
user = User.create(name: name, birthday: birthday)
{ user: user }
end
end
end
JSON Scalar
Create a scalar for JSON data:
# app/graphql/types/json_type.rb
module Types
class JsonType < Types::BaseScalar
description "A JSON value"
def self.coerce_input(value, context)
case value
when String
JSON.parse(value)
when Hash, Array
value
else
raise GraphQL::CoercionError, "cannot coerce #{value.inspect} to JSON"
end
end
def self.coerce_result(value, context)
value.to_json
end
end
end
Email Scalar
Create a scalar with validation:
# app/graphql/types/email_type.rb
module Types
class EmailType < Types::BaseScalar
description "A valid email address"
EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
def self.coerce_input(value, context)
if value.match?(EMAIL_REGEX)
value
else
raise GraphQL::CoercionError, "#{value.inspect} is not a valid email"
end
end
def self.coerce_result(value, context)
value
end
end
end
URL Scalar
Create a scalar for URLs:
# app/graphql/types/url_type.rb
module Types
class UrlType < Types::BaseScalar
description "A valid URL"
def self.coerce_input(value, context)
uri = URI.parse(value)
if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
value
else
raise GraphQL::CoercionError, "#{value.inspect} is not a valid URL"
end
rescue URI::InvalidURIError
raise GraphQL::CoercionError, "#{value.inspect} is not a valid URL"
end
def self.coerce_result(value, context)
value
end
end
end
DateTime Scalar
Create a DateTime scalar with timezone support:
# app/graphql/types/date_time_type.rb
module Types
class DateTimeType < Types::BaseScalar
description "A datetime value in ISO 8601 format"
def self.coerce_input(value, context)
Time.zone.parse(value)
rescue ArgumentError
raise GraphQL::CoercionError, "cannot coerce #{value.inspect} to DateTime"
end
def self.coerce_result(value, context)
value.utc.iso8601
end
end
end
Money Scalar
Create a scalar for monetary values:
# app/graphql/types/money_type.rb
module Types
class MoneyType < Types::BaseScalar
description "A monetary value in cents"
def self.coerce_input(value, context)
case value
when Integer
value
when Float
(value * 100).to_i
when String
(Float(value) * 100).to_i
else
raise GraphQL::CoercionError, "cannot coerce #{value.inspect} to Money"
end
end
def self.coerce_result(value, context)
(value / 100.0).round(2)
end
end
end
Base Scalar Class
Create a base class for your scalars:
# app/graphql/types/base_scalar.rb
module Types
class BaseScalar < GraphQL::Schema::Scalar
end
end
Registering Scalars
Make sure your scalars are loaded. In your schema:
# app/graphql/your_schema.rb
class YourSchema < GraphQL::Schema
# Scalars are automatically discovered if they're in Types::
end
Using Custom Scalars in Queries
Query with custom scalars:
query GetUser($birthday: Date!) {
user(birthday: $birthday) {
id
name
birthday
}
}
Variables:
{
"birthday": "1990-01-15"
}
Testing Custom Scalars
Test your custom scalars:
# spec/graphql/types/date_type_spec.rb
require 'rails_helper'
RSpec.describe Types::DateType do
describe '.coerce_input' do
it 'parses valid date strings' do
result = described_class.coerce_input('2020-01-15', nil)
expect(result).to eq(Date.parse('2020-01-15'))
end
it 'raises error for invalid dates' do
expect {
described_class.coerce_input('invalid', nil)
}.to raise_error(GraphQL::CoercionError)
end
end
describe '.coerce_result' do
it 'formats dates as ISO 8601' do
date = Date.parse('2020-01-15')
result = described_class.coerce_result(date, nil)
expect(result).to eq('2020-01-15')
end
end
end
Best Practices
- Validate input: Always validate input in
coerce_input - Consistent output: Use consistent formats in
coerce_result - Clear errors: Provide helpful error messages
- Documentation: Add descriptions to your scalars
- Reusability: Create base classes for common patterns
Common Patterns
Enum-like Scalar
module Types
class StatusType < Types::BaseScalar
VALID_VALUES = %w[active inactive pending].freeze
def self.coerce_input(value, context)
if VALID_VALUES.include?(value)
value
else
raise GraphQL::CoercionError, "Status must be one of: #{VALID_VALUES.join(', ')}"
end
end
def self.coerce_result(value, context)
value
end
end
end
Conclusion
Custom scalars in GraphQL-ruby allow you to handle specific data types that aren't covered by built-in scalars. By implementing coerce_input and coerce_result, you can create type-safe, validated scalars that improve your GraphQL API's type system and provide better developer experience.