Comparing field arguments and directives
The same functionality to modify the output of a field in GraphQL can often be achieved via two different methods:
- Field arguments:
field(arg: value)
- Query-type directives:
field @directive
(Query-type directives are those applied on the query in the client-side, as contrasted to schema-type directives, which are applied via SDL -Schema Definition Language- when building the schema on the server. As Gato GraphQL creates the schema from PHP code, not from SDL, its directives are all of the query type, and they are simply referenced as "directives".)
For instance, converting the response of a title
field to uppercase could be achieved by passing a field arg format
with an enum value UPPERCASE
, like this:
or by applying a directive @strUpperCase
on the field, like this:
In both cases, the response from the GraphQL server will be the same:
When should we use field arguments and when query-side directives? Is there any difference between the two methods, or any situation when one option is better than the other?
What field arguments and directives are good for
Resolving a field in GraphQL involves two different operations:
- fetching the requested data from the queried entity
- applying functionality (such as formatting) on the requested data
We can label these two operations as "data resolution" and "applying functionality", or, for short, as "data" and "functionality" respectively.
The main difference between field arguments and directives is that field arguments can be used for both "data" and "functionality", but directives can only be used for "functionality".
Let's see a bit more in detail what this means.
Resolving data via field arguments
Field arguments are processed when resolving the field, hence they can be used to retrieve the actual data, such as deciding what property from the object is accessed.
For instance, this resolver code shows how argument size
is used to fetch one or another image source from the Media
object type:
Field args can also be used to help decide what row or column from the DB table must be queried.
In this query, field argument id
is used to query some specific entity of type Post
, which the resolver will translate to some specific row from WordPress' wp_posts
DB table:
The same table stores the post's date in two different columns, post_modified
and post_modified_gmt
(for backward-compatibility reasons). In this query, passing field argument gmt
with true
or false
translates into fetching the value from one or the other column:
These examples demonstrate that field args can modify the source of the data when resolving the field.
Directives cannot be used to modify the source of the data, because their logic is provided via directive resolvers, which are invoked after the field resolver. Hence, by the time the directive is applied, the field's value must have been retrieved.
For instance, this query will never work:
In this example, field post
requires to be provided the id
of the entity, and since it is not provided as a field argument, the server will return an error:
In conclusion, only field arguments can help retrieve the data that resolves the field.
Applying functionality via field arguments or directives
Once we retrieve the data for the field, we may want to manipulate its value. For instance, we could:
- Format a string, converting it to upper or lower case
- Format a date represented with a string, from the default
YYYY-mm-dd
format todd/mm/YYYY
- Mask a string, replacing emails and telephone numbers with
***
- Provide a default value if it is
null
or empty - Round floats to 2 digits
Any of these operations is a manipulation on the already-retrieved data. Hence, they can be coded both in the field resolver, right after fetching the data and before returning it, or in the directive resolver, which will get the field's value as its input. As such, any of these operations can be implemented via either field arguments or directives.
For instance, the field resolver for Post.excerpt
could provide a default value via a field arg default
, and then we can customize the value for the default
arg in the query:
We can also create a @default
directive, with a directive resolver like this:
Are these two strategies equally suitable? Let's explore this question based on different areas of interest.
Field arguments are better covered by the GraphQL spec
The extent to which directives are allowed to operate is not clearly defined in the GraphQL spec, which reads:
Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
In some cases, you need to provide options to alter GraphQL’s execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.
This definition consents to the use of directives such as @include
and @skip
, which conditionally include and skip a field respectively, and @stream
and @defer
, which provide a different runtime execution for retrieving data from the server.
However, this definition is not unambiguous concerning directives which modify the value of a field, such as @strUpperCase
, which transforms the output value "Hello world!"
into "HELLO WORLD!"
.
Due to this ambiguity, different GraphQL servers, clients and tools may take directives into account to different extents, creating conflicts among them.
And example of this is Relay, which does not take directives into account for caching field values. If first querying:
...Relay will query and cache value "Hello world!"
for post with ID 1
. If then we run this query:
...the response should be "HELLO WORLD!
", however Relay will return "Hello world!"
, which is the value stored in its cache for post with ID 1
, ignoring the directive applied on the field.
If directives are allowed to modify the field's output value or not is in a gray area, since it is neither explicitly allowed or disallowed in the GraphQL spec, yet there are indicators for both opposite situations.
On one side, the GraphQL spec seems to grant directives a free hand to improve and customize GraphQL:
As future versions of GraphQL adopt new configurable execution capabilities, they may be exposed via directives. GraphQL services and tools may also provide any additional custom directive beyond those described here.
On the other hand, the spec does not take directives into account for the FieldsInSetCanMerge
validation or the CollectFields
algorithm. The following GraphQL query is valid, yet it is uncertain what response the user will obtain:
Depending on the behavior from the GraphQL server, the response for field name
may be "Leo"
, "LEO"
or "leo"
... we don't know in advance, and that's a problem.
The same issue does not happen with field arguments. When the following query is executed:
...the spec dictates the GraphQL server to return an error, so the value for name
will be null
. We would then be forced to introduce aliases to execute the query:
Directives are better for modularity and code re-usability
Many of the operations offered by directives are agnostic of the entity and field where it is applied. For instance, @strUpperCase
will work on any string, whether applied on a post's title, a user's name, a location's address or anything else.
As a consequence, the code for this directive is implemented only once and in a single place, the directive resolver. Similar to aspect-oriented programming (which increases modularity by allowing the separation of cross-cutting concerns), directives are applied on the field without affecting the logic of the field.
In contrast, implementing the same functionality via a field argument involves executing the same code across the field resolver (and across different field resolvers):
To reduce the amount of code in the resolvers, then directives are more suitable than field arguments.
Directives are better for schema design
Adding field arguments will add extra information to the schema, possibly bloating it and making it inconsistent.
For instance, a field argument format
will need to be added to all String
fields and, if we are not careful, it may not be homogeneous across fields, such as using different names, different values, different default values, or even splitting the argument into several inputs:
Directives allow us to keep the schema as lean as possible:
Directives can be more efficient than field arguments
On execution time, a field argument will be accessed when resolving the field, which happens on a field-by-field and object-by-object basis. For instance, when resolving fields title
and content
on a list of posts, the resolver will be invoked once per post and field:
Imagine that we want to translate these strings using the Google Translate API, for which we add argument translateTo
:
Because the logic is naturally executed per combination of field and object, we may end up requesting a great number of connections to the external API, producing a slow response to resolve the query.
In addition, executing the calls independently from each other will not allow to associate their data, so the quality of the translation will be inferior than if all data were submitted together in a single API call.
For instance, a post title "Power"
can be better translated if the post content, which makes it evident this word refers to "electrical power", is submitted together with it.
Gato GraphQL invokes a directive only once, passing all fields and objects to be applied to as input. By receiving all data all together, the @strTranslate
directive can execute a single call to Google Translate passing along all title
and content
fields for all objects, as in this query:
Directives can provide a more performant way to modify the value of the fields, such as when interacting with external APIs.