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` 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 (anAbsinthe.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.