Proposed by:
- Alex Reilly - Yelp iOS
- Liz Jakubowski - Yelp iOS
- Mark Larah - Yelp Web
- Sanae Rosen - Yelp Android
- Stephen Spalding - Netflix GraphQL Server Infrastructure
- Wei Xue - Yelp iOS
- Young Min Kim - Netflix UI
This RFC proposes syntax that would allow developers to override schema-defined nullability of fields for individual operations.
- Required field - A field which is marked with
!
.
In our experience, client developers have been frustrated that the vast majority of fields are nullable. We’ve done this in accordance with official best practice, and we largely agree that this is good practice. From the official GraphQL best practice:
This is because there are many things that can go awry in a networked service backed by databases and other services. A database could go down, an asynchronous action could fail, an exception could be thrown. Beyond simply system failures, authorization can often be granular, where individual fields within a request can have different authorization rules.
The problem with the SDL Non-Nullable (!
) is that it eliminates the possibility of partial failure
on a given type. This forces schema authors to decide for which fields partial failure is
acceptable. A GraphQL schema author may not be in the best position to predict whether partial failure
will be acceptable or unacceptable for every canvas that makes use of a field.
While the schema can have nullable fields for valid reasons (such as federation), in some cases the
client wants to decide if it accepts a null
value for the result to simplify the client-side
logic.
A client-controlled Non-Nullable designator.
Each client-controlled nullability designator overrides the schema-defined nullability of the field it's attached to for the duration of the operation.
The proposed client-controlled required designator would have identical semantics to the current schema-defined Non-Null.
If a developer executed an operation with two fields name foo
, one a String
and the other an
Int
, the operation would be declared invalid by the server. The same is true if one of the fields
is designated required but both are otherwise the same type. In this example, nickname
could be
either a String
or a String!
which are two different types and therefor can not be merged:
fragment conflictingDifferingResponses on Pet {
... on Dog {
nickname
}
... on Cat {
nickname!
}
}
The client can express that a schema field is required by using the !
syntax in the operation
definition:
query GetBusinessName($id: String!) {
business(id: $id)? {
name!
}
}
We have chosen !
because !
is already being used in the GraphQL spec to indicate that a field in
the schema is Non-Nullable, so it will feel familiar to GraphQL developers.
Handling nullable values on the client is a major source of frustration for developers, especially when using types generated by client code generators in strongly-typed languages. The proposed required designator would allow GraphQL clients to generate types with more precise nullability requirements for a particular feature. For example, using a GraphQL client like Apollo GraphQL on mobile, the following query
query GetBusinessName($id: String!) {
business(id: $id)? {
name!
}
}
would be translated to the following type in Swift.
struct GetBusinessNameQuery {
let id: String
struct Data {
let business: Business?
struct Business {
/// Lack of `?` indicates that `name` will never be `null`
let name: String
}
}
}
If a null business name is not acceptable for the feature executing this query, this generated type is more ergonomic to use since the developer does not need to unwrap the value each time it’s accessed.
Marking a field Non-Nullable in schema is not possible in every use case. For example, when a developer is using a 3rd-party API such as Github's GraphQL API they won't be able to alter Github's schema, but they may still want to have certain fields be required in their application. Even within an organization, ownership rules may dictate that an developer is not allowed to alter a schema they utilize.
- Non-nullable syntax that is based off of syntax that developers will already be familiar with
- Enable GraphQL client code generation tools to generate more ergonomic types
This syntax consciously does not cover the following use cases:
- Default Values
The syntax being used in this proposal causes queries to propagate an error in the case that
a
null
is found for a required field. As an alternative, some languages provide syntax (eg??
for Swift) that says "if a field would benull
return some other value instead". We have not covered that behavior in this proposal, but leave it open to be covered by future proposals.
This solution offers the same benefits as the proposed solution. Additionally, this solution has
good upgrade paths if we later want to provide more behavior options to developers.
Relay's @required
directive, for example,
allows developers to decide how they want their clients to respond in the event that null
is
received for a @required
field.
fragment Foo on User {
address @required(action: THROW) {
city @required(action: LOG)
}
}
With our current proposal, we don't have a great way to offer this kind of flexibility that would mesh nicely with existing GraphQL syntax.
However we think the described behavior acts as a nice, concise default, and is worth the tradeoff.
This is an alternative being used at some of the companies represented in this proposal.
While this solution simplifies some client-side logic, it does not meaningfully improve the developer experience for clients.
- The cache implementations of "smart" GraphQL clients also need to understand the custom directive
to behave correctly. Currently, when a client library caches a
null
field based on an operation without a directive, it will return thenull
field for another operation with this directive. - For clients that rely on client code generation, generated types typically cannot be customized based on a custom directive. See dotansimha/graphql-code-generator#5676 for an example. As a result, the optional generated properties still need to be unwrapped in the code.
This feels like a common enough need to call for a language feature. A single language feature would enable more unified public tooling around GraphQL.
It is intuitive that one should simply mark fields that are not intended to be null
Non-Nullable
in the schema. For example, in the following GraphQL schema:
type Business {
name: String
isStarred: Boolean
}
If we intend to always have a name
and isStarred
for a Business
, it may be tempting to mark
these fields Non-Nullable:
type Business {
name: String!
isStarred: Boolean!
}
Marking schema fields Non-Nullable may introduce problems in a distributed environment where partial
failure is a possibility regardless of whether the field is intended to have null
as a valid
state.
When a Non-Nullable field results in null
, the GraphQL server will recursively step through the
field’s ancestors to find the next nullable field. In the following GraphQL response:
{
"data": {
"business": {
"name": "The French Laundry",
"isStarred": false
}
}
}
If isStarred
is Non-Nullable but resolves to null
and business is nullable, the result will be:
{
"data": {
"business": null
}
}
Even if isStarred
resolves to a valid result, the response would no longer provide this data. If business is
Non-Nullable, the response will be:
{
"data": null
}
In the case that the service storing business stars is unavailable, the UI may want to go ahead and
render the component without a star (effectively defaulting isStarred
to false
). A Non-Nullable
field in the schema makes it impossible for the client to receive partial results from the server,
and thus potentially forces the entire component to fail to render.
More discussion on when to use Non-Nullable can be found here
Also see 3rd-party GraphQL APIs for an instance where it wouldn't be possible for a developer to alter the schema for a service they're using.
This is the alternative being used at some of the companies represented in this proposal for the time being. It's labor intensive and rote work. It more or less undermines any gains from code generation.
This would follow the precedent set by Kotlin. It's more verbose and diverges from GraphQL's SDL precedent.
At the July 26th meeting of the CCN sub-WG it was decided to simplify the RFC for the sake of progress. This
reverses the "?
as a counterpart to the !
" designator.
The increased scope of the RFC with the nullability designator ?
has stalled the RFC for more than two
years (the initial RFC from April 2021).
One of the contentious discussions we encountered was around the proposed behaviors of the nullability designator
?
. Regardless of the chosen path, it had unintuitive semantics (see graphql/nullability-wg#2),
which require other RFCs, such as fragment boundaries, or new ideas like inline errors.
Even though we recognize the value of having a counterpart to !
, we made a tradeoff decision to provide value
to the community sooner than later.
This proposal started out with a very simple premise and implementation, and has gotten more complex as
the community has explored edge cases and facets about how GraphQL is actually used in practice. For
example this proposal starts out by talking about accommodating the "best practices" that are recommended
by the GraphQL documentation and the community, but we discovered pretty early on that there are
legitimate use cases where the "best practices" are rightfully ignored. Some of those use cases are
covered in "?
as a counterpart to !
".
In order to cover instances like that, we've needed to justify additional complexity which can be difficult to understand for newcomers without (at this point a full year) of context. This decision log was written with newcomers in mind to avoid rediscussing issues that have already been hashed out, and to make it easier to understand why certain decisions have been made. At the time of writing, the decisions here aren't set in stone, so any future discussions can use this log as a starting point.
Lee was the first person to suggest
that the inverse of !
should exist and that it should be represented by ?
. The
reasoning was that it "completes
the story of control" and provides a guaranteed stopping point for null
propagation if we're using the existing
null
propagation rules. The feeling was that "introducing ! without ? is like introducing throw
without catch
".
Lee also surfaced that there are some use cases like his own at Robinhood where they're trying to balance
developer experience and data preservation, and have opted to mark quite a few fields Non-Null
. Data
preservation is very important because Robinhood is working with financial data, so they have the opposite
problem where they sometimes want to be able to halt null
propagation, rather than the inverse use case which
this proposal originally supported.
Developers from Apollo indicated that many of their customers face problems around schema breaking where the
solution to developer experience gripes is to make a breaking change and swap a field from nullable to Non-Nullable
or vice versa, which can be a labor intensive process.
Since there seemed to be general consensus that ?
was a good addition to the proposal, it was adopted without a vote.
Subsequently there was discussion around whether ?
could be introduced in a later proposal, and there was general
agreement that the usability of !
is limited without ?
, and the selected null
propagation behavior described
below solidifies the decision to introduce both additions in a single proposal.
Developers from Apollo suggested early on that users would want to apply CCN syntax to list elements. The possibility had been suggested earlier than that as well, but it was put off because neither Netflix nor Relay's CCN counterparts had the feature, and it hadn't been a problem yet. However there was enough interest during community feedback sessions to adopt it into the proposal. Discussions around which specific syntax to adopt happened over the following months.
Options other than the one that was landed on included the following:
twoDimensionalList!!?
The folks that voted against this option felt that it was unclear how it should be interpreted, whether operators should be applied from the outside-in, or inside-out.
twoDimensionalList as [[Int!]!]
twoDimensionalList <= [[Int!]!]
The folks that voted against this option felt that this option read like a type-cast, and that the inclusion of a type placed an undue burden on client developers. Validation would fail if the type was incorrect, and didn't provide much additional value.
twoDimensionalList[[!]!]?
This syntax, called "the bracket syntax" during discussions was selected for adoption by majority vote at the
March 3rd, 2022 GraphQL Working Group Meeting.
9 out of 10 participants voted for this option with the final vote going to the <= [[Int!]!]
option.
Initially there was a restriction on the bracket syntax where the depth of the syntax needed to match depth of the field's list type, but participants at the same meeting felt that restriction should be loosened so that developers could opt to apply the syntax to only the field itself and ignore the elements of the list. Under that new rule the following would also be valid, and the two examples would be equivalent.
twoDimensionalList!
twoDimensionalList[[]]!
There are however some open concerns that the first of the two examples could be ambiguous as to whether
the !
applies to the field as a whole or to the list elements.
The selected mechanics were most requested by the folks at Meta working on Relay. Relay wanted this behavior for a few reasons
- Relay presents a facade of fragment isolation for its own
@required
directive. If a field isnull
, rather than merging fragments and propagatingnull
to all sibling fields on selection sets that use that fragment, the most popular option as chosen by ~90% of developers is to have the request throw and utilize React's error boundaries. It also has the option to donull
propagation, but that too is bound to a single fragment. Because of this, it likely won't be able to use CCN at first, but developers would like to be able to use it in the future once Fragment Modularity makes it into the spec. The selected option preserves the possibility of Relay and other clients that utilize fragments heavily using CCN in the future. - The throwing option that Relay provides on their
@required
directive effectively allows developers to indicate "If some required field is missing, throw out everything from this field to the fragment boundary" which is tighter client control than was offered by the initial iteration of this proposal where!
only transformed a field into aNon-Null
. In that case, the server still had control over wherenull
propagation halted with which fields the schema said were nullable. The selectednull
propagation option is slightly closer to the most popular@required
option in that way.
With the initial iteration of this proposal if users wanted to guarantee that all fields through multiple
parents should be lost in the event that a child is null
, they would need to mark each level with a !
,
but the selected option avoids that.
With the selected option, forgetting to include a ?
is potentially dangerous because it would result
in more fields being lost than intended, all the way up to the data
field in the worst case scenario.
There were concerns that that was a blocker, but arguments were made that because most queries are relatively
small, it wasn't actually that dangerous. Clients have also often been treating the existence of any errors
as a failed request and thrown out entire responses, so in effect, clients been choosing the "worst case
scenario" when given the option.
The behavior where !
propagates null
to nearest ?
was selected for adoption by majority vote at the
March 3rd, 2022 GraphQL Working Group Meeting.
7 out of 8 participants voted for this option with the final vote going to behavior where the !
would be non-destructive.
The non-destructive option was turned down because having different behavior per-client wasn't desirable, and it provided no benefits to naive clients (like a bash script) because extra processing would be required for it to be a value-add.