Custom scalars¶
Custom scalars allow you to convert your Python objects to JSON-serializable form in query results, as well as convert those JSON forms back to Python objects back when they are passed as arguments or input
values.
Example read-only scalar¶
Consider this API defining Story
type with publishedOn
:
type_defs = """
type Story {
content: String
publishedOn: String
}
"""
The publishedOn
field’s resolver returns instance of type datetime
, but in API this field is defined as String
. This means that our datetime will be passed throught the str()
before being returned to client:
{
"publishedOn": "2018-10-26 17:28:54.416434"
}
This may look acceptable, but there are better formats to serialize timestamps for later deserialization on the client, like ISO 8601. We could perform this conversion in our resolver:
def resolve_published_on(obj, *_):
return obj.published_on.isoformat()
…but now you will have to remember to define custom resolver for every field that receives datetime
as value. This really adds up the boilerplate to our API, and makes it harder to use abstractions auto-generating the resolvers for you.
Instead, you could tell GraphQL how to serialize dates by defining custom scalar type:
type_defs = """
type Story {
content: String
publishedOn: Datetime
}
scalar Datetime
"""
If you will try to query this field now, you will get error:
{
"errors": [
{
"message": "Expected a value of type \"Datetime\" but received: 2018-10-26 17:39:55.502078",
"path": [
"publishedOn"
]
}
],
}
This is because our custom scalar has been defined, but it’s currently missing logic for serializing Python values to JSON form.
We need to add special resolver named serialize
to our Datetime
scalar that will implement the logic we are expecting:
def serialize_datetime(value):
return value.isoformat()
resolvers = {
"Datetime": {
"serialize": serialize_datetime
}
}
Doing so will make GraphQL server use custom logic whenever value that is not None
is returned from resolver:
{
"publishedOn": "2018-10-26T17:45:08.805278"
}
Now we can reuse our custom scalar across the API to serialize datetime
instances in standardized format that our clients will understand.
Scalars as input¶
What will happen if now we create field or mutation that defines argument of the type Datetime
? We can find out using basic resolver:
type_defs = """
type Query {
stories(publishedOn: Datetime): [Story!]!
}
"""
def resolve_stories(*_, **data):
print(data.get("publishedOn")) # what value will "publishedOn" be?
data.get("publishedOn")
will return False
, because that is fallback value of read-only scalars.
To turn our read-only scalar into bidirectional scalar, we will need to add two functions to serialize
that was implemented in previous step:
parse_value(value)
that will be used when scalar value is passed as part of queryvariables
.parse_literal(ast)
that will be used when scalar value is passed as part of query content (eg.{ stories(publishedOn: "2018-10-26T17:45:08.805278") { ... } }
).
Those functions can be implemented as such:
def parse_datetime_value(value):
try:
# dateutil is provided by python-dateutil library
return dateutil.parser.parse(value)
except (ValueError, TypeError):
return None
def parse_datetime_literal(ast):
value = str(ast.value)
return parse_value(value) # reuse logic from parse_value
resolvers = {
"Datetime": {
"serialize": serialize_datetime,
"parse_value": parse_datetime_value,
"parse_literal": parse_datetime_literal,
}
}
There’s few things happening in above code, so let’s go through them step by step:
There aren’t any checks to see if arguments passed to function are None
because if that is the case, GraphQL server skips our parsing step altogether.
When value is incorrect and either ValueError
and TypeError
exceptions are raised, they are silenced and function returns None
instead. GraphQL server interprets this as sign that entered value is incorrect because it can’t be transformed to internal representation, and returns appropriate error to the client. This is preferable approach to raising exceptions from parser, because in such case those exceptions interrupt query execution logic (prohibiting possible partial result), as well as result in error messages possibly leaking implementation details to the client.
If value is passed as part of query content, it’s ast
node is instead passed to parse_datetime_literal
to give it chance to introspect type of node (implementations for those be found here), but we are opting in for just extracting whatever value this ast node had, coercing it to str
and reusing parse_value
.