📢 Proposal for "Embeddable fields" in GraphQL

— 14 minute read

I'd like to make a proposal for a new feature for GraphQL, not to be appended to the official spec, but as an optional feature that GraphQL servers may decide to support (similar to the GraphQL Cursor Connections spec).

This post is part of the groundwork to find out if there is support for this feature within the GraphQL community. If there is, only then I'll submit it as a new issue to the GraphQL spec repo for a thorough discussion, and offer to become its champion.

Note: This feature is already supported by GraphQL by PoP. Click on the "Run" button on the GraphiQL clients throughout this post, to execute the query and see the expected response.


Proposed new feature: "Embeddable fields" permalink

Embeddable fields is a syntax construct, that enables to resolve a field within an argument for another field from the same type, using the mustache syntax {{field}}.

Note: To make it convenient to use, field echoStr(value: String): String can be added to the schema, as in the examples shown throughout this post.

This query contains embedded fields {{title}} and {{date}}:

The syntax can contain whitespaces around the field: {{ field }}.

This query contains embedded fields {{ title }} and {{ date }}:

The embedded field may or may not contain arguments:

  • {{ fieldName }}
  • {{ fieldName(fieldArgs) }}

This query formats the date: date(format: \"d/m/Y\"):

Note: The string quotes must be escaped: \"

Embedded fields also work within directive arguments.

This query resolves field title only if the same post has comments:

Note: Using embeddable fields together with directives @skip and @include is an interesting use case. However, condition if expects a Boolean, not a String; even though the query can be resolved properly in the server, there is type mismatch in the client.

This proposal may suggest to accept embedded fields also on their own, and not only within a string, so they can be casted to their own type: @skip(if: {{ hasComments }}). More on this below.

This query resolves field title in two different ways, depending on the post having comments or not:

Benefits of this new feature permalink

Why would we want a GraphQL query to support embeddable fields? The following are benefits I've identified so far.

It lessens the need for a client to process the response permalink

In most situations, we have a client to request the data from the GraphQL server and transform it into the required format.

For instance, a website on the client-side can process the data with JavaScript, as to transform fields title and date into a description:

const desc = `Post ${ response.data.title } was published on ${ response.data.date }`

However, in some situations we may need to retrieve the data for a service that we do not control, and which does not offer tools to process the results.

For instance, a newsletter service (such as Mailchimp) may accept to define an endpoint from which to retrieve the data for the newsletter. Whatever data is returned by the endpoint is final; it can't be manipulated before being injected into the newsletter.

In these situtations, the query could use embeddable fields to manipulate the response into the required format. This could be particularly useful when accessing GraphQL over HTTP.

It can help declutter the schema permalink

The use case above could also be satisfied by adding an extra field Post.descriptionForNewsletter to the schema. But this solution clutters the schema, and embeddable fields could be considered a more elegant solution.

It improves the development experience permalink

Embeddable fields could be compared to arrow functions in JavaScript, which is syntactic sugar over a feature already available in the language.

Arrow functions are not really needed, but they provide benefits:

  • they shorten the amount of code needed to achieve something
  • they simplify the syntax

As such, the feature becomes a welcome-to-have in the language, producing a better development experience.

The if condition in @skip and @include can become dynamic permalink

Currently, argument "if" for the @skip and @include directives can only be an actual boolean value (true or false) or a variable with the boolean value. This behavior is pretty static.

Embeddable fields would enable to make this behavior more dynamic, by evaluating the condition on some property from the object itself.

There is an issue to address: if is a Boolean, not a String, so to avoid type conflicts the GraphQL syntax may also need to accept the embedded field on its own, not wrapping it between string quotes:

query {
posts {
id
title @skip(if: {{ hasComments }})
}
}

Removing the need to wrap {{ }} between quotes " " would solve this issue for every scalar type other than String, not just Boolean (check the example below with droid, using embeddable fields to resolve an ID).

It enables to code templates within the GraphQL query permalink

Embeddable fields enable to embed a template within the GraphQL query itself, which would render the GraphQL service more configuration-friendly.

For instance, combined with the flat chain syntax and nested mutations (two other features also proposed for the spec), we could produce the following query, which sends an email to the user notifying that his/her comment was replied to:

mutation {
comment(id: 1) {
replyToComment(data: data) {
id @sendEmail(
to: "{{ parentComment.author.email }}",
subject: "{{ author.name }} has replied to your comment",
content: "
<p>On {{ comment.date(format: \"d/m/Y\") }}, {{ author.name }} says:</p>
<blockquote>{{ comment.content }}</blockquote>
<p>Read online: {{ comment.url }}</p>
"
)
}
}
}

It could satisfy "exporting variables between queries" permalink

Proposed feature [RFC] exporting variables between queries attempts to @export the value of a field, and inject it into another field in the same query:

query A {
hero {
id @export(as: "droidId")
}
}

query B($droidId: String!) {
droid (id: $droidId) {
name
}
}

With embeddable fields and the flat chain syntax, this use case could be satisfied like this:

query {
droid (id: {{ hero.id }} ) {
name
}
}

Backwards compatibility permalink

This feature breaks backwards compatibility. From the spec:

Once a query is written, it should always mean the same thing and return the same shaped result. Future changes should not change the meaning of existing schema or queries or in any other way cause an existing compliant GraphQL service to become non-compliant for prior versions of the spec.

In our case, if a query currently has this shape:

query {
foo: echoStr(value: "Hello {{ world }}!")
}

...it expects the response to be:

{
"data": {
"foo": "Hello {{ world }}!"
}
}

With embeddable fields the query above will produce a different response and, moreover, it may even produce an error message, as when there is no field Root.world.

In addition, considering the case of not wrapping {{ }} between string quotes " ", as in the query below:

query {
posts {
id
title @skip(if: {{ hasComments }})
}
}

Currently, this query would produce a syntax error, being displayed in the GraphiQL client, and possibly not parsed by the server. This behavior would change.

Because of being backwards incompatible, it is suggested to make embeddable fields an opt-in feature, prompting users to be fully aware of the consequences before enabling it.

Further research permalink

Embeddable fields would affect some components from the GraphQL workflow. How should these be dealt with?

GraphiQL integration permalink

The GraphiQL client shows an error message when a field does not exist, or if a field argument receives a value with a different type than declared in the schema, among other potential errors. Can this information be conveyed for embeddable fields too?

For this to happen, GraphiQL would need to parse the field argument inputs and identify all {{ fieldName(fieldArgs) }} instances, as to do the validations and show the error messages.

Behavior when field is not found permalink

What happens when an embedded field does not exist? For instance, if in the query below, field {{ name }} exists but {{ surname }} does not:

{
users {
fullName: echoStr(value: "{{ name }} {{ surname }}")
}
}

Should the response produce an error message, and skip processing the field? Eg:

{
"errors": [
"Field 'surname' does not exist, so 'echoStr(value: \"{{ name }} {{ surname }}\")' cannot be resolved"
]
}

Or should the missing field be skipped but still resolve the field, and possibly show a warning? Eg:

{
"warnings": [
"Field 'surname' does not exist"
],
"data": {
"users": [
{
"fullName": "Juan {{ surname }}"
},
{
"fullName": "Pedro {{ surname }}"
},
{
"fullName": "Manuel {{ surname }}"
}
]
}
}

Or should the failing field be removed altogether? (Notice there's still a space at the end of each resolved value):

{
"warnings": [
"Field 'surname' does not exist"
],
"data": {
"users": [
{
"fullName": "Juan "
},
{
"fullName": "Pedro "
},
{
"fullName": "Manuel "
}
]
}
}

Escaping {{ field }} permalink

If we actually want to print the string "{{ field }}" in the response, without resolving it, how should it be done?

Previous literature permalink

This feature is a less ambitious version of composable fields, differing in these aspects:

  • It's not meant to be part of the GraphQL spec, but as an attached optional spec, and offered as an opt-in feature by the GraphQL server
  • If resolved only within a string, embeddable fields would not require a change to the GraphQL syntax
  • Having a field resolve the value for another field happens only 1 level down, not multiple-levels down as with composable fields

Current implementations permalink

Embeddable fields are supported in GraphQL server GraphQL by PoP, and its implementation for WordPress GraphQL API for WordPress, in both as an opt-in feature.

Join the discussion! permalink

If there is enough support for this feature, I will add an RFC issue to the GraphQL spec. Everyone is welcome to provide feedback in this Reddit post:

  • Do you support embeddable fields in GraphQL? Would you benefit from it? How?
  • Are you against embeddable fields? Why?