October 3, 2023

GraphQLNullable is not a trap!

Calvin Cestari

Calvin Cestari

Nullability is a fundamental concept in many programming languages giving us the ability to express the absence of a value or assigned object. This is particularly relevant in strongly typed languages where each constant or variable must be defined with one of the language data types or explicitly allowed to be null.

GraphQL is strongly-typed and all fields are, by default, nullable with null being a valid response for any field unless explicitly declared otherwise. In GraphQL syntax a trailing exclamation mark is used to denote a field that uses a Non-Null type, like this:

{
  nullableField: String
  nonNullableField: String!
}

Strong typing is also one of the defining characteristics of Swift where the use of the Optional type ensures that nullable (nil) values are handled explicitly. Swift default type behavior is the opposite of GraphQL, though, and types are defined as non-null unless explicitly declared otherwise. The question mark on a type declaration indicates that the value it contains is optional (nullable), like this:

let nonOptionalValue: String
let optionalValue: String?

Note: GraphQL has concepts of both Nullable and Optional, and there is semantic overlap with the Swift <a href="https://developer.apple.com/documentation/swift/optional">Optional</a> type. It is worth taking the time to understand each to avoid confusion.

Nullability in generated Swift

In the section above, we showed how nullability is an integral part of both languages. Let’s explore how nullability is used in the Swift code generated by Apollo iOS.

During code generation when a GraphQL field type is defined as nullable, Apollo iOS will translate that into a Swift Optional type, allowing the generated model to receive null as a valid response for that field.

type Query {
  allAnimals: [Animal]
}

interface Animal {
  species: String # <- nullable
}

type Predator implements Animal {
  species: String # <- nullable
}

type Pet implements Animal {
  species: String # <- nullable
  name: String # <- nullable
}
query AllAnimals {
  allAnimals {
    species
  }
}
class AllAnimalsQuery: GraphQLQuery {
  struct Data: SelectionSet {
    struct AllAnimal: SelectionSet {
      var species: String? { get } // <- optional
    }
  }
}

Note: There are cases where non-nullable fields will be generated as nullable by using the @include or @skip directives. In these cases, the field can be returned as null and therefore a Swift Optional type is needed.

If you’ve used Apollo iOS to generate Swift code before you might have noticed that some fragment accessors are generated as optional too. This is because the fragment’s fields might not be returned in the response depending on the object’s type.

query AllAnimals {
  allAnimals {
    species
    ... on Pet {
      ...PetDetails
    }
  }
}

fragment PetDetails on Pet {
  name
}
class AllAnimalsQuery: GraphQLQuery {
  struct Data: SelectionSet {
    struct AllAnimal: SelectionSet {
      var species: String? { get } // <- optional
      var asPet?: AsPet { _asInlineFragment() } // <- optional
      
      struct AsPet: InlineFragment {
        var name: String? { get } // <- optional
      }
    }
  }
}

Where is GraphQLNullable?

So far we’ve explored nullability in GraphQL and Swift, but we still haven’t encountered the GraphQLNullable type yet. Looking at the way nullable types are defined in both languages it is reasonable to assume that they can be expressed in the same way, albeit with slightly different syntax. However, that’s not true.

A Swift Optional either has a value, which must be unwrapped, or there is no value. Similarly, GraphQL types either have a value or there is no value but, most importantly, null values have two semantically different ways to represent the lack of a value: explicitly by providing the literal value: null; implicitly by not providing a value at all. Trying to reconcile this difference is where GraphQLNullable is required.

n the same way that Swift’s Optional type is an enum, GraphQLNullable is also a Swift enum providing the some and none cases but additionally the third possible case allowed by GraphQL – null.

  • some represents the presence of a value
  • none represents the implicit absence of a value, aka nil
  • null represents an explicitly null value

GraphQLNullable is not needed in the generated response models though because whether a GraphQL response contains null for a field or the field is not present, both are represented in the generated response as a Swift optional. The only time a field is allowed to be missing from a GraphQL response is as a result of the @include, @skip or @defer directives. Any other time a field is missing would indicate a partial response due to a field error.

It is only on input types where the subtle difference between nil and null becomes important. In a mutation or input variable, an explicit null value could be taken to represent the deletion of a field, whereas the absence of an explicit value (i.e.: nil) could be taken to imply no change to the field. Unfortunately, some GraphQL server implementations simply treat them the same, and many developers who use GraphQL are unaware of the difference. This is also in contrast to type-safe languages which cannot discern the difference between explicit or implicit nil.

But why?

Apollo iOS is a general-purpose GraphQL client and must comply with the full GraphQL specification. We needed to make it clear that there is a difference, and you should be explicit in what you want. Therefore, the design of GraphQLNullable forces the user to select a case that signals their intent without question.

GraphQLNullable was introduced with the major 1.0 release and is, without doubt, an improvement over the previous syntax of double-optional. Developers were often confused by this syntax where an inner optional with a value of nil combined with an outer optional value of some would mean the GraphQL value null, while an outmost optional of nil would mean the absence of a value, aka. nil. We received plenty of questions about why values were doubly wrapped in optional and it would often lead to easy mistakes in using nil when you actually wanted null but the compiler could never know what your intent was.

The change to GraphQLNullable hasn’t been without friction, though, and most of the feedback we have received is from developers who say they do not need to be concerned with the difference between nil and null. We believe in the value of being explicit, but it does make the usage of these fields more cumbersome because of having to use the verbose syntax of .some() or GraphQLNullable(), especially when combined with another enum type such as GraphQLEnum.

Examples

Consider the following definitions:

struct Filter {
  let species: String
  let name: String? = nil
  let predator: Bool
}

var filter: GraphQLNullable

// Implicitly stating the absence of a value, aka nil.
filter = .none 

// Explicitly stating a null value
filter = .null

// Stating the presence of a value
filter = .some(.init(
  species: "Canine", 
  predator: false
))

As you can see above, the none and null cases are easy enough to use and clear in their semantics, whereas having to wrap values in some is awkward and less clear to someone reading the code who may be unfamiliar with GraphQLNullable.

For additional convenience, you’ll find that GraphQLNullable conforms to Swift’s ExpressibleBy protocols that match GraphQL’s scalar types, such as ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, etc. So if your GraphQLNullable property is wrapping a GraphQL scalar type, then initialization can be done as follows:

let aString: GraphQLNullable = "value"
let aBool: GraphQLNullable = true
let aInt: GraphQLNullable = 123
let aDouble: GraphQLNullable = 1.1

GraphQLNullable also has a convenient nil coalescing operator useful in cases where the value being evaluated is optional itself:

let species: GraphQLNullable = optionalValue ?? .null

Improvements

Despite the conveniences, we’ve seen that the some case is still awkward to use. For those who aren’t concerned about the semantic difference of nil and null there have been suggestions from the community for an extension on GraphQLNullable that can help with the initialization of nullable values.

extension GraphQLNullable {
  init(optionalValue value: Wrapped?) {
    if let value {
      self = .some(value)
    } else {
      self = .none // <- change this to .null if your server requires
    }
  }
}

Note: We choose not to provide this extension by default in Apollo iOS because the decision to return .none vs. .null is going to be specific to how your connecting GraphQL server interprets nullable values. You should take the time to fully understand the impact of this extension and what the resultant input field value is signaling to your server.

Wrapping up

GraphQL and Swift are a good fit. They are both strongly typed and have nullability built-in. Their syntax is quite similar even if the defaults are different and the details of handling null vs nil can get complicated for input values.

I hope this article cleared up some of the confusion around GraphQLNullable and provided you with some insight into its design.

We welcome your feedback, and please don’t hesitate to reach out on GitHub, the Apollo Community, or the Apollo Discord server.

Written by

Calvin Cestari

Calvin Cestari

Read more by Calvin Cestari