Skip to content

Latest commit

 

History

History
342 lines (262 loc) · 13.6 KB

QueryLevelNullability.md

File metadata and controls

342 lines (262 loc) · 13.6 KB

RFC: Operation Level Nullability

Proposed by:

This RFC proposes a syntactical construct for GraphQL clients to express that fields in an operation are non-null.

Definitions

  • Nullability. A feature of many programming languages (eg Swift, Kotlin, SQL) that is used to indicate whether or not a value can be null.

    Nullability language constructs (e.g. ? in Swift/Kotlin) have become popular due to their ability to solve ergonomic problems in programming languages, such as those surrounding NullPointerException in Java.

  • Codegen. Short for "code generation", in this proposal refers to tools that generate code to facilitate using GraphQL on the client. GraphQL codegen tooling exists for many platforms:

    • Apollo has a code generator for Android (Kotlin) and iOS (Swift) clients
    • The Guild has a TypeScript code generator for web clients

    GraphQL codegen tools typically accept a schema and a set of documents as input, and output code in a language of choice that represents the data returned by those operations.

    For example, the Apollo iOS codegen tool generates Swift types to represent each operation document, as well as model types representing the data returned from those queries. Notably, a nullable field in the schema becomes an Optional property on the generated Swift model type, represented by ? following the type name.

    In the example below, the Business schema type has a nullable field called name.

    # Schema
    type Business {
      # The unique identifier for the business (non-nullable)
      id: String!
    
      # The name of the business (nullable)
      name: String
    }
    
    # Document
    query GetBusinessName($id: String!) {
      business(id: $id) {
        name
      }
    }

    At build time, Apollo generates the following Swift code (note: the code has been shortened for clarity).

    struct GetBusinessNameQuery {
      let id: String
    
      struct Data {
        let business: Business?
    
        struct Business {
          /// Since the `Business.name` schema field is nullable, the corresponding codegen Swift property is `Optional`
          let name: String?
        }
      }
    }

    The query can then be fetched, and the resulting data handled, as follows:

    GraphQL.fetch(query: GetBusinessNameQuery(id: "foo"), completion: { result in
      guard case let .success(gqlResult) = result, let business = gqlResult.data?.business else { return }
    
      // Often, the client needs to provide a default value in case `name` is `null`.
      print(business?.name ?? "null")
    }

📜 Problem Statement

The two modern languages used on mobile clients, Swift and Kotlin, are both non-null by default. By contrast, in a GraphQL type system, every field is nullable by default. 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.

This discrepancy means that developers using strongly-typed languages must perform null checks anywhere that GraphQL codegen types are used, significantly decreasing the benefits of codegen. In some cases, the codegen types may be so complex that a new model type which encapsulates the null handling is written manually.

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. In addition, syntax for this concept would allow codegen tooling to generate model types that are more ergonomic to work with, since the model type's properties would have the desired nullability.

🧑‍💻 Proposed Solution

A client specified Non-Null designator.

🎬 Behavior

The proposed query-side Non-Null designator would have identical semantics as the current SDL-defined Non-Null. Specifically:

  • If the result of resolving a field is null (either because the function to resolve the field returned null or because an error occurred), and that field is of a Non-Null type, or the operation specifies this field as Non-Null, then a field error is thrown. The error must be added to the "errors" list in the response.

  • Since Non-Null type fields cannot be null, field errors are propagated to be handled by the parent field. If the parent field may be null then it resolves to null, otherwise the field error is further propagated to its parent field.

  • If all fields from the root of the request to the source of the field error return Non-Null types or are specified as Non-Null in the operation, then the "data" entry in the response should be null.

✏️ Proposed syntax

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 is non-nullable. Incidentally the same precedent exists in Swift (!) and Kotlin (!!) which both use similar syntax to indicate "throw an exception if this value is null".

Use cases

When a field is necessary to the function of the client

Expressing nullability in the operation, as opposed to the schema, offers the client more flexibility and control over whether or not an error is thrown.

There are cases where a field is nullable, but a feature that fetches the field will not function if it is null. For example if you are trying to render an information page for a movie, you won't be able to do that if the name field of the movie is missing. In that case it would be preferable to fail as early as possible.

According to the official GraphQL best practice, this field should be non-nullable:

When designing a GraphQL schema, it's important to keep in mind all the problems that could go wrong and if "null" is an appropriate value for a failed field. Typically it is, but occasionally, it's not. In those cases, use non-null types to make that guarantee.

However, the same field may not be "vital" for every feature in the application that fetches it. Marking the field as non-null in the schema would result in those other features erroring unnecessarily whenever the field is "null".

When handling partial results

It is often unclear how to handle partial results.

  • What elements are essential to having an adequate developer experience? How many elements can fail before you should just show an error message instead?
  • When any combination of elements can fail, it is very hard to anticipate all edge cases and how the UI might look and work, including transitions to other screens.

Implementing these decisions significantly complicates the client-side logic for handling query results.

✨ Examples

Non-nullable field

query GetBusinessName($id: String!) {
  business(id: $id) {
    name!
  }
}

would codegen to the following type on iOS.

struct GetBusinessNameQuery {
  let id: String

  struct Data {
    let business: Business?

    struct Business {
      /// Lack of `?` indicates that `name` will never be `null`
      let name: String
    }
  }
}

Non-nullable object with nullable fields

query GetBusinessReviews {
  business(id: 4) {
    reviews! {
      rating
      text
    }
  }
}

✅ RFC Goals

  • Non-nullable syntax that is based off of syntax that developers will already be familiar with
  • Enable GraphQL codegen tools to generate more ergonomic types

🚫 RFC Non-goals

🗳️ Alternatives considered

A @nonNull official directive

This solution offers the same benefits as the proposed solution. Since many GraphQL codegen tools already support the @skip and @include directives, this solution likely has a faster turnaround.

A @nonNull custom directive

This is an alternative being used at some of the companies represented in this proposal for the time being.

While this solution simplifies some client-side logic, it does not meaningfully improve the developer experience for clients.

  • The cache implementations of GraphQL client libraries 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 the null field for another operation with this directive.
  • For clients that rely on codegen, codegen types typically cannot be customized based on a custom directive. See dotansimha/graphql-code-generator#5676 for an example. As a result, the optional codegen 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.

Make Schema Fields Non-Nullable Instead

It is intuitive that one should simply mark fields that are not intended to be null as non-null 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 as Non-Null:

    type Business {
      name: String!
      isStarred: Boolean!
    }

Marking Schema fields as non-null can introduce particular problems in a distributed environment where there is a possibility of partial failure 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 returns null and business is nullable, the result will be:

{
  "data": {
    "business": null
  }
}

Even if name returns valid results, 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 user stars is unavailable, the UI may want to go ahead and render the component without a star (effectively defaulting isStarred to false). Non-Null 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 rendering.

More discussion on when to use non-null can be found here

Additionally, marking a field non-null 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 non-nullable in their application.

Write wrapper types that null-check fields

This is the alternative being used at some of the companies represented in this proposal for the time being. It's quite labor intensive and the work is quite rote. It more or less undermines the purpose of having code generation.

Alternatives to !

!!

This would follow the precedent set by Kotlin.

Make non-nullability apply recursively

For example, everything in this tree would be non-nullable

query! {
  business(id: 4) {
    name
  }
}

🙅 Syntax Non-goals

This syntax consciously does not cover the following use cases:

  • Default Values The syntax being used in this proposal causes queries to error out in the case that a null is found. As an alternative, some languages provide syntax (eg ?? for Swift) that says "if a value would be null make it some other value instead". We are not interested in covering that in this proposal.

Work Items

Patches that will need to be made if this proposal is accepted. The RFC proposal process requires that a proof of concept is implemented in a GraphQL library. As work items are completed, PRs will be linked here.