View Source 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
endScalar definitions, created using the scalar macro from
Absinthe.Schema.Notation, are made up of two major elements:
- A function provided to
parsethat 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
serializethat 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
endThen, 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
parsefunction 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.