Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Directive proposal for opting out of null bubbling #1050

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions spec/Section 2 -- Language.md
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,11 @@ NonNullType :
- NamedType !
- ListType !

NullOnlyOnErrorType :

- NamedType \*
- ListType \*

GraphQL describes the types of data expected by arguments and variables. Input
types may be lists of another input type, or a non-null variant of any other
input type.
Expand Down
174 changes: 173 additions & 1 deletion spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,7 @@ non-null input type as invalid.
**Type Validation**

1. A Non-Null type must not wrap another Non-Null type.
1. A Non-Null type must not wrap a Null-Only-On-Error type.

### Combining List and Non-Null

Expand Down Expand Up @@ -1892,6 +1893,83 @@ Following are examples of result coercion with various types and values:
| `[Int!]!` | `[1, 2, null]` | Error: Item cannot be null |
| `[Int!]!` | `[1, 2, Error]` | Error: Error occurred in item |

## Null-Only-On-Error

The GraphQL Null-Only-On-Error type is an alternative to the GraphQL Non-Null
type to disallow null unless accompanied by a field error. This type wraps an
underlying type, and this type acts identically to that wrapped type, with the
exception that {null} will result in a field error being raised. A trailing
asterisk is used to denote a field that uses a Null-Only-On-Error type like
this: `name: String*`.

Null-Only-On-Error types are only valid for use as an _output type_; they must
not be used as an _input type_.

**Nullable vs. Optional**

Fields that return Null-Only-On-Error types will never return the value {null}
if queried _unless_ an error has been logged for that field.

**Result Coercion**

To coerce the result of a Null-Only-On-Error type, the coercion of the wrapped
type should be performed. If that result was not {null}, then the result of
coercing the Null-Only-On-Error type is that result. If that result was {null},
then a _field error_ must be raised.

Note: When a _field error_ is raised on a Null-Only-On-Error value, the error
does not propagate to the parent field, instead {null} is used for the value.
For more information on this process, see
[Handling Field Errors](#sec-Handling-Field-Errors) within the Execution
section.

**Input Coercion**

Null-Only-On-Error types are never valid inputs.

**Type Validation**

1. A Null-Only-On-Error type must wrap an _output type_.
1. A Null-Only-On-Error type must not wrap another Null-Only-On-Error type.
1. A Null-Only-On-Error type must not wrap a Non-Null type.

### Combining List and Null-Only-On-Error

The List and Null-Only-On-Error wrapping types can compose, representing more
complex types. The rules for result coercion of Lists and Null-Only-On-Error
types apply in a recursive fashion.

For example if the inner item type of a List is Null-Only-On-Error (e.g.
`[T*]`), then that List may not contain any {null} items unless associated field
errors were raised. However if the inner type of a Null-Only-On-Error is a List
(e.g. `[T]*`), then {null} is not accepted without an accompanying field error
being raised, however an empty list is accepted.

Following are examples of result coercion with various types and values:

| Expected Type | Internal Value | Coerced Result |
| ------------- | --------------- | ------------------------------------------- |
| `[Int]*` | `[1, 2, 3]` | `[1, 2, 3]` |
| `[Int]*` | `null` | `null` (With logged coercion error) |
| `[Int]*` | `[1, 2, null]` | `[1, 2, null]` |
| `[Int]*` | `[1, 2, Error]` | `[1, 2, null]` (With logged error) |
| `[Int!]*` | `[1, 2, 3]` | `[1, 2, 3]` |
| `[Int!]*` | `null` | `null` (With logged coercion error) |
| `[Int!]*` | `[1, 2, null]` | `null` (With logged coercion error) |
| `[Int!]*` | `[1, 2, Error]` | `null` (With logged error) |
| `[Int*]` | `[1, 2, 3]` | `[1, 2, 3]` |
| `[Int*]` | `null` | `null` |
| `[Int*]` | `[1, 2, null]` | `[1, 2, null]` (With logged coercion error) |
| `[Int*]` | `[1, 2, Error]` | `[1, 2, null]` (With logged error) |
| `[Int*]!` | `[1, 2, 3]` | `[1, 2, 3]` |
| `[Int*]!` | `null` | Error: Value cannot be null |
| `[Int*]!` | `[1, 2, null]` | `[1, 2, null]` (With logged coercion error) |
| `[Int*]!` | `[1, 2, Error]` | `[1, 2, null]` (With logged error) |
| `[Int*]*` | `[1, 2, 3]` | `[1, 2, 3]` |
| `[Int*]*` | `null` | `null` (With logged coercion error) |
| `[Int*]*` | `[1, 2, null]` | `[1, 2, null]` (With logged coercion error) |
| `[Int*]*` | `[1, 2, Error]` | `[1, 2, null]` (With logged error) |

## Directives

DirectiveDefinition : Description? directive @ Name ArgumentsDefinition?
Expand Down Expand Up @@ -1940,7 +2018,8 @@ by a validator, executor, or client tool such as a code generator.

:: A _built-in directive_ is any directive defined within this specification.

GraphQL implementations should provide the `@skip` and `@include` directives.
GraphQL implementations should provide the `@skip`, `@include` and
`@nullOnError` directives.

GraphQL implementations that support the type system definition language must
provide the `@deprecated` directive if representing deprecated portions of the
Expand Down Expand Up @@ -2162,3 +2241,96 @@ to the relevant IETF specification.
```graphql example
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
```

### @nullOnError

```graphql
directive @nullOnError on QUERY | MUTATION | SUBSCRIPTION
```

The `@nullOnError` _built-in directive_ may be provided on query, mutation and
subscription operations, and disables the error propagation behavior described
in [Handling Field Errors](#sec-Handling-Field-Errors) by treating all Non-Null
types as if they were instead Null-Only-On-Error types.

Note: This is useful for clients that still wish to receive sibling fields when
an error on a Non-Null value occurs. Effectively, `@nullOnError` enables the
client to opt in to handling errors locally; for example, a client might use
this to limit the scope of null propagation to a fragment rather than the entire
field, or to update a normalized store even when an error occurs.

Consider the following schema:

```graphql
type Query {
me: Viewer
}

type Viewer {
username: String!
bestFriend: Viewer!
}
```

If the `bestFriend` field were to return `null`, then the following operation:

```graphql example
query myQuery {
me {
username
bestFriend {
username
}
}
}
```

Would return a result such as:

```json example
{
"errors": [
{
"message": "Value cannot be null",
"locations": [{ "line": 4, "column": 5 }],
"path": ["me", "bestFriend"]
}
],
"data": {
"me": null
}
}
```

However, if we apply the `@nullOnError` directive to our operation:

```graphql example
query myQuery @nullOnError {
me {
username
bestFriend {
username
}
}
}
```

The result would contain identical errors, but the "me" field will be populated:

```json example
{
"errors": [
{
"message": "Value cannot be null",
"locations": [{ "line": 4, "column": 5 }],
"path": ["me", "bestFriend"]
}
],
"data": {
"me": {
"username": "billy",
"bestFriend": null
}
}
}
```
46 changes: 43 additions & 3 deletions spec/Section 4 -- Introspection.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,14 @@ enum __TypeKind {
INPUT_OBJECT
LIST
NON_NULL
NULL_ONLY_ON_ERROR
}

type __Field {
name: String!
description: String
args(includeDeprecated: Boolean = false): [__InputValue!]!
type: __Type!
type(includeNullOnlyOnError: Boolean! = false): __Type!
isDeprecated: Boolean!
deprecationReason: String
}
Expand Down Expand Up @@ -263,6 +264,7 @@ possible value of the `__TypeKind` enum:
- {"INPUT_OBJECT"}
- {"LIST"}
- {"NON_NULL"}
- {"NULL_ONLY_ON_ERROR"}

**Scalar**

Expand Down Expand Up @@ -400,12 +402,35 @@ required inputs for arguments and input object fields.

The modified type in the `ofType` field may itself be a modified List type,
allowing the representation of Non-Null of Lists. However it must not be a
modified Non-Null type to avoid a redundant Non-Null of Non-Null.
modified Non-Null type to avoid a redundant Non-Null of Non-Null; nor may it be
a modified Null-Only-On-Error type since these types are mutually exclusive.

Fields\:

- `kind` must return `__TypeKind.NON_NULL`.
- `ofType` must return a type of any kind except Non-Null.
- `ofType` must return a type of any kind except Non-Null and
Null-Only-On-Error.
- All other fields must return {null}.

**Null-Only-On-Error**

GraphQL types are nullable. The value {null} is a valid response for field type.

A Null-Only-On-Error type is a type modifier: it wraps another _output type_
instance in the `ofType` field. Null-Only-On-Error types do not allow {null} as
a response _unless_ an associated _field error_ has been raised.

The modified type in the `ofType` field may itself be a modified List type,
allowing the representation of Null-Only-On-Error of Lists. However it must not
be a modified Null-Only-On-Error type to avoid a redundant Null-Only-On-Error of
Null-Only-On-Error; nor may it be a modified Non-Null type since these types are
mutually exclusive.

Fields\:

- `kind` must return `__TypeKind.NULL_ONLY_ON_ERROR`.
- `ofType` must return a type of any kind except Non-Null and
Null-Only-On-Error.
- All other fields must return {null}.

### The \_\_Field Type
Expand All @@ -422,10 +447,25 @@ Fields\:
{true}, deprecated arguments are also returned.
- `type` must return a `__Type` that represents the type of value returned by
this field.
- Accepts the argument `includeNullOnlyOnError` which defaults to {false}. If
{false}, let {fieldType} be the type of value returned by this field and
instead return a `__Type` that represents
{RecursivelyStripNullOnlyOnErrorTypes(fieldType)}.
- `isDeprecated` returns {true} if this field should no longer be used,
otherwise {false}.
- `deprecationReason` optionally provides a reason why this field is deprecated.

RecursivelyStripNullOnlyOnErrorTypes(type):

- If {type} is a Null-Only-On-Error type:
- Let {innerType} be the inner type of {type}.
- Return {RecursivelyStripNullOnlyOnErrorTypes(innerType)}.
- Otherwise, return {type}.

Note: This algorithm recursively removes all Null-Only-On-Error type wrappers
(e.g. `[[Int*]!]*` would become `[[Int]!]`). This is to support legacy clients:
they can safely treat a Null-Only-On-Error type as the underlying nullable type.

### The \_\_InputValue Type

The `__InputValue` type represents field and directive arguments as well as the
Expand Down
5 changes: 4 additions & 1 deletion spec/Section 6 -- Execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ field execution process continues recursively.

CompleteValue(fieldType, fields, result, variableValues):

- If the {fieldType} is a Non-Null type:
- If the {fieldType} is a Non-Null or a Null-Only-On-Error type:
- Let {innerType} be the inner type of {fieldType}.
- Let {completedResult} be the result of calling {CompleteValue(innerType,
fields, result, variableValues)}.
Expand Down Expand Up @@ -805,3 +805,6 @@ upwards.
If all fields from the root of the request to the source of the field error
return `Non-Null` types, then the {"data"} entry in the response should be
{null}.

Note: By the above, field errors that happen in `Null-Only-On-Error` types do
not propagate.