The why of GraphQL Client Side Nullability in Examples
A terse exposition of how client side nullability can inform client component design through comprehensive examples.
A nullable field can represent a value that may or may not exist.
Client side nullability can be used to solve common issues when defining the data fetch for client side components by:
- Validating that all the fields the component expects are available in one shot
- Simplifying the types on fetched data from nullable types to non nullable types (especially elegant when using statically typed languages where you have to unwrap the value from an optional type).
- Modifying how errors or null values affect the returned fields based on bubble-up logic
Bubbling on the server (a recap)
Let's set up an example server API to use in the next sections on client side component data fetches.
On the server, every field is nullable by default; the GraphQL spec allows the server to return a "partial response." A resilient API can resolve all the fields it is able to and return errors on the side. Errors can include system failures (network/database/code) or even authorization errors. Nullable by default also eases API evolution by ensuring that clients are responsible for validating fields in the response data.
A null value in a field that is not nullable bubbles up to the nearest nullable field.
For example, with the following query and types:
# sample query 1 BASIC
query {
user {
email
profile { # nullable
picture
address { # not nullable
street # not nullable
country
}
}
}
}
# server schema
type User {
email: String
profile: Profile # fields in graphql are nullable by default
}
type Profile {
picture: String
address: Address! # not nullable
}
type Address {
street: String! # not nullable
country: String
}
If something went wrong while fetching the non-nullable "street" field, then the server would bubble up that error to the nearest nullable field "profile", and you would get a null profile field. For this query, either you get a profile with a street, or no profile at all. This is true even if "picture" was a valid value.
// return when non-nullable street not resolved
{
user: {
email: ...
profile: null // null bubbled up to first nullable field
}
}
// return if street value is resolved
{
user: {
email: ...
profile: {
picture: ...
address: {
street: ... // street has a non null value
country: ...
}
}
}
}
Moving Control to the Client With Nullability Designators
Client side nullability introduces two new operators !
named "required" and ?
named "optional" that the client can use to specify how the server should bubble up null errors.
Solution 1: Narrow optional data with smaller error boundary in a profile widget
The client can force the server to return some data while marking other data as optional.
# sample query 2 PROFILE WIDGET, smaller boundary
query {
user {
email
profile { # nullable
picture
address? { # not nullable, optional
street! # not nullable, required
country
}
}
}
}
In case of an error or null value while resolving the required field "street", the null value would propagate up to the closest optional field "address", not the closest nullable field. So this allows for a client side developer to override overzealous nullability requirements that the server specifies and get this response:
// sample response 2 PROFILE WIDGET
{
user: {
email: ...
profile: {
picture: ...
address: null // closest optional field is null
}
}
}
The profile widget can now display a profile picture even when the address is invalid.
Solution 2: Simplify data validation with larger error boundary in a location widget
The client can indicate that either all or no data should be returned to simplify validation of the returned data.
# sample query 3 USER LOCATION WIDGET, larger boundary
query {
user? { # optional
email
profile { # nullable
picture
address { # not nullable
street! # not nullable, required
country
}
}
}
}
A null value for the required street field still propagates to the closest optional field, so we get this result:
// sample response 3 USER LOCATION WIDGET
{
user: null
}
This means that the user location widget doesn't need to dig into the internals of the returned user data and validate that the fields returned as expected, the null at the optional field already captures the bubbled up error.
Solution With Relay
The Relay library has had this for some time through the @required directive specific to the Relay library, and different from the client side nullability GraphQL spec.
query {
user {
email
... profileFragment
}
}
fragment profileFragment on users {
profile {
picture
address @required {
street @required
country
}
}
}
If "street", an @required field is missing, the null value will bubble up to the first field that is not @required, but only within the same fragment i.e. the "profile" field.
{
user: {
email: ...
profile: null
}
}
This is because Relay likes to use fragments to locally scope data fetching requirements with data masking. An advantage with the Relay approach is that the directives are implemented on the client, the server does not need to support the @required directive, unlike the client side nullability spec.
Relay is also looking to push the client side nullability spec further with Fragment Response Keys which define fragment composition boundaries for the client side nullability designators (GraphQL Fails Modularity on YouTube).
Using this today
The client side nullability spec is currently in the RFC stage, and Hasura does not support it today.
You can however use the @required directive with Relay today, and Hasura can generate a Relay API. Check it out if the idea of having data requirements colocated with your components using fragments stands out to you. It also makes for a phenomenal developer experience.
If you're using Hasura backed by a single PostgreSQL database, you don't really have to worry about the server returning unexpected null values.
In a federated setup, with Hasura backed by multiple data sources across the network, something like client side nullability could help clients account for partial failures when resolving data. Hasura v3 will handle these scenarios more elegantly by allowing partial fetches even with some of the backing data sources down.
From a client perspective, a developer probably wants to focus on nailing down data requirements for every component regardless of the status of the backing data sources.
The GraphQL spec continues to evolve to solve real problems that developers encounter.
Here are some active discussions for GraphQL features that are still in the works! GraphQL Working Group RFCs
References
https://github.com/graphql/graphql-wg/blob/main/rfcs/ClientControlledNullability.md
http://spec.graphql.org/October2021/#sec-Handling-Field-Errors
https://relay.dev/docs/next/guides/required-directive/
https://hasura.io/blog/graphql-nulls-cheatsheet/
Other discussions:
https://github.com/graphql/graphql-wg/discussions/1009
https://github.com/graphql/graphql-wg/discussions/994
https://www.youtube.com/watch?v=SVx4HG2bhII