Concerning if GraphQL should support recursions in fragments or not, it has been argued that GraphQL does not need to support features found in programming languages (such as recursions) because GraphQL itself is not a programming language:

Roman: these same problems (infinite loops) apply to all programming languages and programming systems.

Michael: GraphQL is not a programming language.

Matt: ... GraphQL's goal is very different from an arbitrary programming language.

To counter this idea there is the principle of least astonishment (POLA), which proposes that a component of a system should behave in a way that most users will expect it to behave. In other words, even if GraphQL is not a programming language, if its users expect it to behave in a certain way (because they are influenced by their coding experience, or some other reason) then GraphQL should not "surprise" them with an unexpected behavior:

Even if GraphQL is really VERY different - many things still apply. The principle of the 'least surprise'. Whenever we introduce a concept that is similar to something that the reader already knows, we should not 'surprise' with behavior that is different from reader's expectations. But if it is different, then it requires lengthy explanations of this difference in the spec. And the reader has to remember that "X is different in GraphQL'. We must try to reduce the 'mental load' on readers, cut-off all the unneeded 'fat' - unless it is really important for us and GraphQL to be different.

I think the GraphQL spec may be failing to comply with POLA in several places. For this article I have compiled a list of features, or lack of them, where GraphQL might be said to provide some level of suprise. As the list ended up being a bit long, I've split it into 2 chapters. This is the first part.

Recursions

(I will use PHP code to demonstrate what can be done with a programming language.)

In PHP we can execute recursions, i.e. executing a function that invokes itself:

function fibonacci(int $num): int
{
  if ($num < 0) {
    throw new Exception("Invalid input");
  }

  if ($num === 0 || $num === 1) {
    return $num;
  }
  
  return fibonacci($num - 1) + fibonacci($num - 2);
}

As PHP does not prevent us from executing any particular type of recursion, we may produce an endless loop that could crash the server, render it unavailable for other processes, or some other negative effect. This code, for instance, will loop forever:

function append(string $text): string
{
  return append("appending: " . $text);
}

Because of the possibility of infinite loops, recursive fragments are banned in GraphQL, since malicious actors could execute such a query against a GraphQL API and bring the service down. Hence, the following query will not be executed and return an error instead:

query GetPostComments($postID: ID!) {
  post(id: $postID) {
    comments {
      ...CommentData
    }
  }
}

fragment CommentData on Comment {
  id
  content
  date
  responses {
    ...CommentData
  }
}

Recursions in fragments have been requested in issues #91, #237 and #929.

Declaring the type of dynamic variables

In PHP, we can optionally indicate what is the type of an input to the function. In this case, input $number is defined to be an integer:

function double(int $number): int
{
  return $number * 2;
}

Similarly, in GraphQL we must indicate the type of a variable. In this case, $postID is of type ID:

query GetPost($postID: ID!) {
  post(id: $postID) {
    title
    content
  }
}

Now, when declaring a variable within the PHP function, we do not indicate its type; the variable's type is determined by the context in which the variable is used. In the code below, assigning an integer value to $double will make this variable be an integer:

function double(int $number): int
{
  // This var is an integer, but we don't need to declare it
  $double = $number * 2;
  return $double;
}

Through custom directives, GraphQL can also support dynamic variables, where a dynamic variable has its value obtained when resolving the query in the server, instead of being provided by the client.

We can - theoretically - accomplish this via custom directive @export, to export the value of a field into a variable, and then have this variable passed as an argument to some other field:

query SearchRecordsContainingUserEmail($userEmail: String!) {
  loggedInUserEmail @export(as: "userEmail")
  records(filter: { search: $userEmail }) {
    id
    title
    content
  }
}

As it can be seen in the query above, even though the variable $userEmail is dynamic, it must still be defined as being of type String in the operation. That is because the GraphQL spec does not differentiate between "static" and "dynamic" variables, and the specified behavior corresponds to static variables.

If we executed the query without defining the variables in the operation, like this:

query SearchRecordsContainingUserEmail {
  loggedInUserEmail @export(as: "userEmail")
  records(filter: { search: $userEmail }) {
    id
    title
    content
  }
}

...then the GraphQL server would return an error:

{
  "errors": [
    {
      "message": "Variable '$userEmail' has not been defined"
    }
  ]
}

Better support for dynamic variables has been requested in #583.

Nesting functionality

In PHP, we can modify the result of a function by applying another function to it. For instance, this code converts a post title to uppercase:

$postTitleInUppercase = strtoupper(getTitle($post));

To modify the value of a field in GraphQL we could apply a custom directive on it:

{
  post(id: 1) {
    title @upperCase
  }
}

However, whenever a directive modifies the value of the field, applying the directive in the query (as opposed to applying the directive to the schema's field definition, via SDL) can break compatibility with other GraphQL tools and clients. Relay, for instance, does not factor directives in when caching field values, hence to Relay caching the results from the query above is not different than from the one without @upperCase:

{
  post(id: 1) {
    title
  }
}

Then, if the post title "What a beautiful evening" has been requested and converted to uppercase, its cached result "WHAT A BEAUTIFUL EVENING" will from then on be returned even when not requesting the field in uppercase format.

The underlying reason for this behavior is that the GraphQL spec is ambiguous on how directives can be used:

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.

Since the spec does not explicitly state that directives can modify field values, and it doesn't ban it either, different GraphQL tools might interpret this section in different ways. If we want to avoid potential incompatibilities among GraphQL clients, whenever we need to modify a field value via the query, we must use a field argument instead of a directive.

This is a pity. If the GraphQL spec explicitly supported modifying field values via directives, Relay could be adapted, and the incompatibilities would be resolved. Then, nesting functionality in GraphQL via directives would be straightforward.

Currently, because using custom directives is discouraged, nesting functionality can still be done but in an awkward/bloated/hacky way. The technique is to use the @export directive (as described earlier on) to export the value of a field into a dynamic variable, and then pass this variable as input to a field upperCase (hence this function has been converted from directive to field) defined in the QueryRoot type:

type QueryRoot {
  upperCase(text: String!): String
}
query GetPostTitleInUppercase($postTitle: String!) {
  post(id: 1) {
    title @export(as: "postTitle")
  }
  postTitle: upperCase(text: $postTitle)
}

Nesting functionality on List elements

In PHP, we can iterate over the items of an array and invoke a functionality to modify their values:

$postTitles = [
  1 => 'What a beautiful evening',
  2 => 'What a wonderful morning',
];
foreach ($postTitles as $id => $title) {
  $postTitles[$id] = strtoupper($title);
}

This logic is decoupled: the iteration of the array, and the method strtoupper, are independent and can be combined in any desired way. Then, PHP does not need to provide a function strtoupperForLists that receives an array as input.

In GraphQL, this is not so easy to accomplish. In the previous section we saw how a field upperCase can transform a String input. But following the same strategy when the input is a list of strings, or [String], we'd have to create another field upperCaseList to deal with it:

type QueryRoot {
  upperCaseList(list: [String!]!): [String!]!
}
query GetPostCategoryNamesInUppercase($postCategoryNames: [String!]!) {
  post(id: 1) {
    categoryNames @export(as: "postCategoryNames")
  }
  postCategoryNames: upperCaseList(list: $postCategoryNames)
}

Even more, we'd also need field upperCaseListOfLists to deal with input of type [[String]], and field upperCaseListOfListOfLists and so on. This is clearly a no-go.

Fortunately, a neat solution is possible: directives could modify the behavior of other directives. This would allow a directive @forEach to iterate the items of the array, and invoke the next directive on the pipeline (such as @upperCase) passing the iterated element as input.

(Even if applying @upperCase to the field in the query is discouraged, as I described earlier on, we can use this same strategy also in the schema's field definition via SDL, which is OK.)

With this solution, instead of creating a directive @upperCaseList, the same functionality can be achieved by applying directives @forEach @upperCase to the field of type [String].

A directive @forEach modifying the behavior of a downstream directive

If the type is [[String]], instead of @upperCaseListOfLists, we can simply do @forEach @forEach @upperCase.

Applying directives on directives is currently not supported by the GraphQL spec, as there are no directive locations DIRECTIVE or DIRECTIVE_DEFINITION.

Support for this feature is currently a work in progress, via PR #907.

Nested mutations

In PHP, we can create a persist method that takes an input and stores it to the DB:

$post = getPost(1);
$post->setTitle("What a incredible autumn");
persist($post);

Alternatively, persist can be a method on the class of the element to be persisted:

$post->persist();

In GraphQL, mutations are placed at the root level, which is the equivalent of persist($post):

mutation {
  updateTitleOnPost(id: 1, title: "What a incredible autumn") {
    title
  }
}

The equivalent for $post->persist() is to execute a nested mutation, placed on the type of the entity being mutated:

mutation {
  post(id: 1) {
    updateTitle(title: "What a incredible autumn") {
      title
    }
  }
}

However, the GraphQL spec declares that mutations must take place at the root-level or, in other words, nested mutations are not allowed.

This is to avoid race conditions, as field must be resolved serially in the root level, but can be resolved in parallel otherwise. If it allowed nested mutations, any of the two mutations in the query below could be executed first, and the value on finalTitle could not be guaranteed:

mutation {
  post(id: 1) {
    updateTitle(title: "What a incredible autumn") {
      title
    }
    updateTitle(title: "What a gorgeous summer") {
      title
    }
    # What will the value be?
    finalTitle: title
  }
}

This is unfortunate, because not all programming languages offer the concept of parallel execution. Concerning PHP, support for it is very recent (only since PHP 8.1, via Fibers), and GraphQL servers in PHP still resolve fields serially. If the GraphQL server doesn't resolve fields in parallel, then it could very well support nested mutations, but the spec prevents this.

Support for nested mutations has been requested in #252.

Conclusion

Even if GraphQL is not a programming language, GraphQL servers and clients do still depend on some programming language, and as such they should share a common behavior, at least as much as possible. According to the principle of least astonishment, we should find out which are those expected behaviors and incorporate them into the GraphQL spec, as to avoid "surprising" developers (i.e. making their work more difficult than it could be).

This article has presented several circumstances in which GraphQL might be deviating from what is expected from it. In the next (and final) article a few more circumstances will complete the list.