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.

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.

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
spec.graphql.org/October2021/#sec-Handling-..
relay.dev/docs/next/guides/required-directive
hasura.io/blog/graphql-nulls-cheatsheet

Other discussions:
https://github.com/graphql/graphql-wg/discussions/1009
github.com/graphql/graphql-wg/discussions/994
youtube.com/watch?v=SVx4HG2bhII

Originally published at hasura.io on July 19, 2023.