diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index 3ac7c7e60..1a8088906 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -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. diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index d32b08566..32e85b9ea 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -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 @@ -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? @@ -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 @@ -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 + } + } +} +``` diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index 3054a9f6c..1d82a1b08 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -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 } @@ -263,6 +264,7 @@ possible value of the `__TypeKind` enum: - {"INPUT_OBJECT"} - {"LIST"} - {"NON_NULL"} +- {"NULL_ONLY_ON_ERROR"} **Scalar** @@ -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 @@ -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 diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f357069f9..f0d06c1d8 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -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)}. @@ -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.