The principle of least astonishment proposes (POLA) that features of some piece of software that produce a high astonishment factor among its users should be redesigned to suit the users' expectations.

GraphQL must not be indifferent to this principle. In my previous article I started analyzing what features from the GraphQL spec produce some level of "surprise" among users. This is the second part.

Union of scalars

In PHP we can declare the inputs (and also the output) of a method to be a union of types, meaning that the parameter can accept any of the types defined in the union, and the union can include any of the scalar types: string, int, float and bool.

For example, this method can receive (and return) an int or float types:

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

While in GraphQL it is possible to declare a union of types, it cannot involve scalar types. Then, this schema definition will not work:

union AnyScalar = String | Int | Float | Bool

The union of scalars has been requested in #215. It will be partially supported when @oneOf is added to the spec.

Mixed pseudotype

In PHP, we can declare a typehint to be the pseudotype mixed, which means that it can accept any type (int, string, float, bool, object, array, null, callable, resource or any class or interface).

We can use mixed to not restrict the input to some method:

function echoInput(mixed $anything): mixed
{
  return $anything;
}

// Passing any type of input will work
printf(echoInput("hi there"));
printf(echoInput(3));
printf(echoInput(bool));
printf(echoInput([1, 2, 3]));

The GraphQL spec does not contain any equivalent pseudotype. If we needed to implement an echo field that mirrors the input back, we'd need to declare several fields instead, at one field per type of input:

type QueryRoot {
  echoString($input: String!): String!
  echoBool($input: Bool!): Bool!
  echoInt($input: Int!): Int!
  echoFloat($input: Float!): Float!
  
  echoListOfString($input: [String!]!): [String!]!
  echoListOfBool($input: [Bool!]!): [Bool!]!
  echoListOfInt($input: [Int!]!): [Int!]!
  echoListOfFloat($input: [Float!]!): [Float!]!
}

We can alternatively implement a custom scalar type Mixed that never fails coercing the input value:

const { GraphQLScalarType } = require('graphql');

const mixedScalar = new GraphQLScalarType({
  name: 'Mixed',
  description: 'Any custom scalar type',
  serialize(value) {
    return value;
  },
  parseValue(value) {
    return value;
  },
  parseLiteral(ast) {
    return ast.value;
  },
});

Then we can reduce the number of fields to add to the schema:

type QueryRoot {
  echoMixed($input: Mixed!): Mixed!

  echoListOfMixed($input: [Mixed!]!): [Mixed!]!
}

However this solution is a hack, as it creates incompatibilities between the server and client, thus breaking the strong contract. That's because the behavior of Mixed is special in that it should be able to receive any scalar type (such as passing String or Int where Mixed is expected), but the GraphQL client may not be aware of it and raise a type-mismatch error:

GraphiQL indicating a type-mismatch error

The Mixed pseudotype was requested in #325 (as the Any scalar type), but it didn't gain support precisely because it would weaken the strong typing from the GraphQL schema. This is a situation in which a feature desired for GraphQL trumps POLA.

Namespacing

In PHP we can namespace classes (and interfaces and traits), which prevents conflicts from different classes having the same name:

namespace PoP\ComponentModel;

class Engine {
  // ...
}

GraphQL can also benefit from namespacing, as to avoid conflicts happening whenever:

  • Two (or more) types or interfaces have the same name
  • Two (or more) fields on the same type or interface have the same name
  • Two (or more) directives have the same name

But the GraphQL spec does not incorporate namespacing as a language feature. Instead, it encourages incorporating a namespace already within the names of the elements in the schema, as suggested for custom directives:

When defining a custom directive, it is recommended to prefix the directive’s name to make its scope of usage clear and to prevent a collision with built-in directive which may be specified by future versions of this document (which will not include _ in their name). For example, a custom directive used by Facebook’s GraphQL service should be named @fb_auth instead of @auth.

Naming elements in such a way that namespacing as a language feature is not needed is the strategy employed by Facebook:

We avoid naming collisions in two ways:

  1. integration tests.

We don't allow any commit to merge into our repository that would result in a broken GraphQL schema. [...]

  1. Common naming patterns.

We have common patterns for naming things which naturally avoid collision problems. [...]

However, this strategy only works when we have full control of the elements in our GraphQL schema. If we incorporate 3rd party elements (such as a directive available via some external library), then their names may or may not be suitable to our naming convention.

Namespacing has been requested in issue #163.

Map

In PHP we can deal with associative arrays (also called maps or dictionaries), where the value is assigned under a named key:

$pageSlugTitles = [
  'about' => 'About us',
  'contact' => 'Contact',
  'blog' => 'Latest blog posts',
];

While GraphQL supports the List type (so that a field of type [String] will return an ordered list of strings), it does not support a Map type, which would allow the results to be organized as pairs of key => value.

Retrieving the results from a dictionary in GraphQL is still doable but it is not so straightforward, as we must first query the two lists (keys and values) separately:

{
  pageSlugs
  pageTitles
}
{
  "data": {
    "pageSlugs": ["about", "contact", "blog"],
    "pageTitles": ["About us", "Contact", "Latest blog posts"]
  }
}

...and then merge the results of the two lists under an object structure in the client:

const merged = Object.assign({}, ...data.pageSlugs.map((n, index) => ({[n]: data.pageTitles[index]})))

Support for the Map type has been requested in #101 and #888.

Tuple

Another data structure that is commonly used in programming languages is the "tuple", which is a sequence of elements, and each element (under a certain position in the array) can declare to have its own type.

In PHP there is no distinct tuple data structure, but we can repurpose an array to behave like a tuple:

// Tuple of [String, Int, Bool, [String]]
$aboutPageData = ["about", 22, true, ["company", "careers"]];

In GraphQL likewise there is no Tuple type, but we also can't use lists to replicate its behavior, since these must always wrap the same type, such as [String] being a list of String; it is not possible to declare a list to contain a String in the first position, an Int in the second, and so on.

Tuples have been requested in #534 and #904.

Enums representing any string value

In PHP, a string value assigned to an enumerator has no restrictions, so it can contain spaces, dashes, non-latin characters and special symbols:

enum GeographicalLocations: string
{
  case NY = 'New York';
  case M = 'München';
  case IL = 'Île-de-France';
  case D = 'الدوحة';
  case B = '北京';
}

This is not the case in GraphQL. On one side, the value of the enum is its name:

enum GeographicalLocations {
  NEW_YORK
  MUNCHEN
  ILE_DE_FRANCE
  DOHA
  BEIJING
}

On the other side, names of entities in GraphQL must respect regex validation ^[a-zA-Z_][a-zA-Z0-9_]*$, which exludes spaces, dashes and special symbols. As no enum can be named "Île-de-France", this value cannot be represented as an enum.

To use enum values with non-supported characters, these must then be defined in the client, and the enum value from the GraphQL response must be mapped into the actual enum value:

const geographicalLocations = {
  NEW_YORK: 'New York',
  MUNCHEN: 'München',
  ILE_DE_FRANCE: 'Île-de-France',
  DOHA: 'الدوحة',
  BEIJING: '北京',
}
const convertedEnumValue = geographicalLocations[data.userLocation];

To be fair, even though I've added this feature to the POLA list, it may actually not belong there: it hasn't been requested for the GraphQL spec, so users are not really expecting it.

Conclusion

In this second second of two articles, we have explored a few additional cases in which GraphQL may be lacking some features that are expected by developers, thus "surprising" them and making their work a bit more difficult. (See the principle of least astonishment.)

As most of the listed examples have been mentioned in one or another issue for the GraphQL spec, and the Working Group keeps iterating and improving the GraphQL spec, we can expect GraphQL's surprise factor is going down.