GraphQLNullable is not a trap!
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
}
}
}
}
GraphQLNullable
?
Where is 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 valuenone
represents the implicit absence of a value, akanil
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.