Custom Scalar Types

One of the strengths of GraphQL is its extensibility---which doesn't end with its object types, but is present all the way down to the scalar value level.

Sometimes it makes sense to build custom scalar types to better model your domain. Here's how to do it.

The GraphQL Specification doesn't define date and datetime types, but Absinthe ships with several pre-built for use via import_types. In this example we'll look at how :datetime is defined.

Defining a scalar

Supporting additional scalar types is as easy as using the scalar macro and providing parse and serialize functions.

Here's the definition for :datetime from Absinthe.Type.Custom:

@desc """
The [`DateTime`](https://hexdocs.pm/elixir/DateTime.html) scalar type represents a date and time in the UTC
timezone. The DateTime appears in a JSON response as an ISO8601 formatted
string, including UTC timezone ("Z"). The parsed date and time string will
be converted to UTC and any UTC offset other than 0 will be rejected.
"""
scalar :datetime, name: "DateTime" do
  serialize &DateTime.to_iso8601/1
  parse &parse_datetime/1
end

@spec parse_datetime(Absinthe.Blueprint.Input.String.t) :: {:ok, DateTime.t} | :error
@spec parse_datetime(Absinthe.Blueprint.Input.Null.t) :: {:ok, nil}
defp parse_datetime(%Absinthe.Blueprint.Input.String{value: value}) do
  case DateTime.from_iso8601(value) do
    {:ok, datetime, 0} -> {:ok, datetime}
    {:ok, _datetime, _offset} -> :error
    _error -> :error
  end
end
defp parse_datetime(%Absinthe.Blueprint.Input.Null{}) do
  {:ok, nil}
end
defp parse_datetime(_) do
  :error
end

Scalar definitions, created using the scalar macro from Absinthe.Schema.Notation, are made up of two major elements:

  • A function provided to parse that defines how input is converted from an AST value (an Absinthe.Blueprint.Input.t) to a value that's suitable for use in an argument and passed to a resolver.
  • A function provided to serialize that defines how input is serialized back out when used as a result that should be sent back to the user.

In this example:

mutation CreatePost {
  post(title: "Second", body: "We're off to a great start!", publishedAt: "2017-11-01T12:00:00Z") {
    id
    publishedAt
  }
}

If our schema defines a :published_at argument with the :datetime type:

field :post, :post do
  arg :published_at, :datetime
  resolve fn _, args, _ ->
    # If the arg is provided, `args.published_at` will be a DateTime struct
    # ...
  end
end

Then, the value of the publishedAt GraphQL field ends up being parsed by the parse_datetime/1 function; it's been defined as the parse function for the :datetime type using the parse macro.

The value, "2017-11-01T12:00:00Z", will come into the function as an %Absinthe.Blueprint.Input.String{}, thanks to the hard work of the Absinthe parser and processing pipeline.

The parse_datetime/1 function pulls the string value out of the input, parses it with DateTime.from_iso8601/1, then returns the correct result. That result will be used as the value of the :published_at argument when it's passed to the :post field resolver.

It's important to note that---currently---the correct result from a scalar's parse function in the event of the error is a lone atom, :error, not an error tuple with a reason. In a future version of Absinthe, custom parse errors may be supported.

The serializer of :datetime is a pretty simple affair; it uses the DateTime.to_iso8601/1 utility function. It would be called to serialize the %DateTime{} struct for the requested :published_at field in the result.

More custom scalar examples can be found under Absinthe Wiki - Scalar Recipes.