From f0e3d6e98179d898d447ddad8a390fc5d3c2369d Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Oct 2023 11:48:13 +0100 Subject: [PATCH 01/10] Add specification changes for Null-Only-On-Error type --- spec/Section 2 -- Language.md | 5 ++ spec/Section 3 -- Type System.md | 74 ++++++++++++++++++++++++++++++ spec/Section 4 -- Introspection.md | 46 +++++++++++++++++-- spec/Section 6 -- Execution.md | 5 +- 4 files changed, 126 insertions(+), 4 deletions(-) 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..7db0e8a95 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,79 @@ 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` | +| `[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]` | `[1, 2, null]` | +| `[Int]*` | `[1, 2, Error]` | `[1, 2, 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` | `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? 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. From 8241d78b68154ce0322fd20dc1fae0ccab9792b6 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Oct 2023 16:24:40 +0100 Subject: [PATCH 02/10] Add examples combining null-only-on-error with list and non-null --- spec/Section 3 -- Type System.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 7db0e8a95..e0fba0751 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1957,10 +1957,18 @@ Following are examples of result coercion with various types and values: | `[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) | From 23fa23b7c06d0cf36b3ce53e377e0789d40c56ff Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Oct 2023 16:24:57 +0100 Subject: [PATCH 03/10] Remove duplicate coercion for brevity --- spec/Section 3 -- Type System.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index e0fba0751..beee413ae 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1949,10 +1949,6 @@ 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` | -| `[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]` | `[1, 2, null]` | From 8395fd58abf3885ddd0e1d3db793ed20deb9efb9 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 24 Nov 2023 10:53:51 +0000 Subject: [PATCH 04/10] Change name to 'SemanticNonNull' and syntax to bang prefix --- spec/Section 2 -- Language.md | 6 +- spec/Section 3 -- Type System.md | 88 +++++++++++++++--------------- spec/Section 4 -- Introspection.md | 46 ++++++++-------- spec/Section 6 -- Execution.md | 6 +- 4 files changed, 72 insertions(+), 74 deletions(-) diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index 1a8088906..f5e245ab3 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -1239,10 +1239,10 @@ NonNullType : - NamedType ! - ListType ! -NullOnlyOnErrorType : +SemanticNonNullType : -- NamedType \* -- ListType \* +- ! 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 diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index beee413ae..3b6445508 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1859,7 +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. +1. A Non-Null type must not wrap a Semantic-Non-Null type. ### Combining List and Non-Null @@ -1893,31 +1893,31 @@ 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 +## Semantic-Non-Null -The GraphQL Null-Only-On-Error type is an alternative to the GraphQL Non-Null +The GraphQL Semantic-Non-Null 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*`. +exception that {null} will result in a field error being raised. A leading +exclamation mark is used to denote a field that uses a Semantic-Non-Null type +like this: `name: !String`. -Null-Only-On-Error types are only valid for use as an _output type_; they must +Semantic-Non-Null 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. +Fields that return Semantic-Non-Null 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 +To coerce the result of a Semantic-Non-Null 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}, +coercing the Semantic-Non-Null 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 +Note: When a _field error_ is raised on a Semantic-Non-Null 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 @@ -1925,50 +1925,50 @@ section. **Input Coercion** -Null-Only-On-Error types are never valid inputs. +Semantic-Non-Null 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. +1. A Semantic-Non-Null type must wrap an _output type_. +1. A Semantic-Non-Null type must not wrap another Semantic-Non-Null type. +1. A Semantic-Non-Null type must not wrap a Non-Null type. -### Combining List and Null-Only-On-Error +### Combining List and Semantic-Non-Null -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 +The List and Semantic-Non-Null wrapping types can compose, representing more +complex types. The rules for result coercion of Lists and Semantic-Non-Null 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. +For example if the inner item type of a List is Semantic-Non-Null (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 Semantic-Non-Null 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) | +| `![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 diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index 1d82a1b08..6d150a52d 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -162,14 +162,14 @@ enum __TypeKind { INPUT_OBJECT LIST NON_NULL - NULL_ONLY_ON_ERROR + SEMANTIC_NON_NULL } type __Field { name: String! description: String args(includeDeprecated: Boolean = false): [__InputValue!]! - type(includeNullOnlyOnError: Boolean! = false): __Type! + type(includeSemanticNonNull: Boolean! = false): __Type! isDeprecated: Boolean! deprecationReason: String } @@ -264,7 +264,7 @@ possible value of the `__TypeKind` enum: - {"INPUT_OBJECT"} - {"LIST"} - {"NON_NULL"} -- {"NULL_ONLY_ON_ERROR"} +- {"SEMANTIC_NON_NULL"} **Scalar** @@ -403,34 +403,32 @@ 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; nor may it be -a modified Null-Only-On-Error type since these types are mutually exclusive. +a modified Semantic-Non-Null 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 and - Null-Only-On-Error. +- `ofType` must return a type of any kind except Non-Null and Semantic-Non-Null. - All other fields must return {null}. -**Null-Only-On-Error** +**Semantic-Non-Null** 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. +A Semantic-Non-Null type is a type modifier: it wraps another _output type_ +instance in the `ofType` field. Semantic-Non-Null 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 +allowing the representation of Semantic-Non-Null of Lists. However it must not +be a modified Semantic-Non-Null type to avoid a redundant Null-Only-On-Error of +Semantic-Non-Null; 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. +- `kind` must return `__TypeKind.SEMANTIC_NON_NULL`. +- `ofType` must return a type of any kind except Non-Null and Semantic-Non-Null. - All other fields must return {null}. ### The \_\_Field Type @@ -447,24 +445,24 @@ 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 + - Accepts the argument `includeSemanticNonNull` 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)}. + {RecursivelyStripSemanticNonNullTypes(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): +RecursivelyStripSemanticNonNullTypes(type): -- If {type} is a Null-Only-On-Error type: +- If {type} is a Semantic-Non-Null type: - Let {innerType} be the inner type of {type}. - - Return {RecursivelyStripNullOnlyOnErrorTypes(innerType)}. + - Return {RecursivelyStripSemanticNonNullTypes(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. +Note: This algorithm recursively removes all Semantic-Non-Null type wrappers +(e.g. `![[!Int]!]` would become `[[Int]!]`). This is to support legacy clients: +they can safely treat a Semantic-Non-Null type as the underlying nullable type. ### The \_\_InputValue Type diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f0d06c1d8..4e7bd0571 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 or a Null-Only-On-Error type: +- If the {fieldType} is a Non-Null or a Semantic-Non-Null type: - Let {innerType} be the inner type of {fieldType}. - Let {completedResult} be the result of calling {CompleteValue(innerType, fields, result, variableValues)}. @@ -806,5 +806,5 @@ 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. +Note: By the above, field errors that happen in `Semantic-Non-Null` types do not +propagate. From bd038f24720b0bf9dc7d9e2eb3edf4f4498a4759 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 3 Jan 2024 22:31:38 +0000 Subject: [PATCH 05/10] Add IsValidImplementationFieldType updates --- spec/Section 3 -- Type System.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 3b6445508..5b7248d2b 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -953,16 +953,23 @@ IsValidImplementationFieldType(fieldType, implementedFieldType): 1. If {fieldType} is a Non-Null type: 1. Let {nullableType} be the unwrapped nullable type of {fieldType}. 2. Let {implementedNullableType} be the unwrapped nullable type of - {implementedFieldType} if it is a Non-Null type, otherwise let it be - {implementedFieldType} directly. + {implementedFieldType} if it is a Non-Null type or Semantic-Non-Null type, + otherwise let it be {implementedFieldType} directly. 3. Return {IsValidImplementationFieldType(nullableType, implementedNullableType)}. -2. If {fieldType} is a List type and {implementedFieldType} is also a List type: +2. If {fieldType} is a Semantic-Non-Null type: + 1. Let {nullableType} be the unwrapped nullable type of {fieldType}. + 2. Let {implementedNullableType} be the unwrapped nullable type of + {implementedFieldType} if it is a Semantic-Non-Null type, otherwise let it + be {implementedFieldType} directly. + 3. Return {IsValidImplementationFieldType(nullableType, + implementedNullableType)}. +3. If {fieldType} is a List type and {implementedFieldType} is also a List type: 1. Let {itemType} be the unwrapped item type of {fieldType}. 2. Let {implementedItemType} be the unwrapped item type of {implementedFieldType}. 3. Return {IsValidImplementationFieldType(itemType, implementedItemType)}. -3. Return {IsSubType(fieldType, implementedFieldType)}. +4. Return {IsSubType(fieldType, implementedFieldType)}. IsSubType(possibleSubType, superType): From 69cab2a78e9ea93c5a5c7c7fdd1f2c0d9c43f724 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 10 Mar 2025 10:33:06 +0000 Subject: [PATCH 06/10] Oops, missed one --- spec/Section 4 -- Introspection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index 6d150a52d..be97fdc2e 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -421,7 +421,7 @@ 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 Semantic-Non-Null of Lists. However it must not -be a modified Semantic-Non-Null type to avoid a redundant Null-Only-On-Error of +be a modified Semantic-Non-Null type to avoid a redundant Semantic-Non-Null of Semantic-Non-Null; nor may it be a modified Non-Null type since these types are mutually exclusive. From 858d409360d30875d6d308e59314b484e6c7399b Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 10 Mar 2025 11:03:22 +0000 Subject: [PATCH 07/10] Add more clarifying text --- spec/Section 3 -- Type System.md | 35 ++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 5b7248d2b..db38f1933 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -328,11 +328,13 @@ scalar string returns either null or a singular string. A GraphQL schema may describe that a field represents a list of another type; the `List` type is provided for this reason, and wraps another type. -Similarly, the `Non-Null` type wraps another type, and denotes that the -resulting value will never be {null} (and that a _field error_ cannot result in -a {null} value). +Similarly, both the `Non-Null` type and the `Semantic-Non-Null` type wrap +another type, the former denotes that the resulting value will never be {null} +(and that a _field error_ cannot result in a {null} value), whereas the latter +denotes that the resulting value will never be {null} _unless_ there is a +matching _field error_. -These two types are referred to as "wrapping types"; non-wrapping types are +These three types are referred to as "wrapping types"; non-wrapping types are referred to as "named types". A wrapping type has an underlying named type, found by continually unwrapping the type until a named type is found. @@ -345,10 +347,13 @@ like Scalar and Enum types, can be used as both input types and output types; other kinds of types can only be used in one or the other. Input Object types can only be used as input types. Object, Interface, and Union types can only be used as output types. Lists and Non-Null types may be used as input types or -output types depending on how the wrapped type may be used. +output types depending on how the wrapped type may be used. Semantic-Non-Null +types may only be used as output types. IsInputType(type) : +- If {type} is a Semantic-Non-Null type: + - Return {false} - If {type} is a List type or Non-Null type: - Let {unwrappedType} be the unwrapped type of {type}. - Return IsInputType({unwrappedType}) @@ -358,7 +363,7 @@ IsInputType(type) : IsOutputType(type) : -- If {type} is a List type or Non-Null type: +- If {type} is a List type, Non-Null type, or Semantic-Non-Null type: - Let {unwrappedType} be the unwrapped type of {type}. - Return IsOutputType({unwrappedType}) - If {type} is a Scalar, Object, Interface, Union, or Enum type: @@ -1902,12 +1907,16 @@ Following are examples of result coercion with various types and values: ## Semantic-Non-Null -The GraphQL Semantic-Non-Null 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 leading -exclamation mark is used to denote a field that uses a Semantic-Non-Null type -like this: `name: !String`. +The Semantic-Non-Null type is an alternative to the Non-Null type, used to +indicate that a field is _semantically_ non-nullable (the data will never +represent a {null}) whilst still allowing the position to act as an error +boundary, preventing error propagation. Essentially it acts to disallow {null} +_unless_ accompanied by a _field error_. + +The Semantic-Non-Null 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 leading exclamation mark is used to denote a +field that uses a Semantic-Non-Null type like this: `name: !String`. Semantic-Non-Null types are only valid for use as an _output type_; they must not be used as an _input type_. @@ -1915,7 +1924,7 @@ not be used as an _input type_. **Nullable vs. Optional** Fields that return Semantic-Non-Null types will never return the value {null} if -queried _unless_ an error has been logged for that field. +queried _unless_ a _field error_ is associated with that field. **Result Coercion** From 9033398793c9d7895044b4668455ca19e47ac294 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 10 Mar 2025 11:03:43 +0000 Subject: [PATCH 08/10] Change syntax to use asterisk again --- spec/Appendix B -- Grammar Summary.md | 7 +++- spec/Section 2 -- Language.md | 6 ++-- spec/Section 3 -- Type System.md | 48 +++++++++++++-------------- spec/Section 4 -- Introspection.md | 2 +- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/spec/Appendix B -- Grammar Summary.md b/spec/Appendix B -- Grammar Summary.md index 92f222cb3..d6b5a117d 100644 --- a/spec/Appendix B -- Grammar Summary.md +++ b/spec/Appendix B -- Grammar Summary.md @@ -43,7 +43,7 @@ Token :: - FloatValue - StringValue -Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | } +Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | } \* Name :: @@ -234,6 +234,11 @@ NonNullType : - NamedType ! - ListType ! +SemanticNonNullType : + +- NamedType \* +- ListType \* + Directives[Const] : Directive[?Const]+ Directive[Const] : @ Name Arguments[?Const]? diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index f5e245ab3..5d28e0a54 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -176,7 +176,7 @@ and is {Ignored}. ### Punctuators -Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | } +Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | } \* GraphQL documents include punctuation in order to describe structure. GraphQL is a data description language and not a programming language, therefore GraphQL @@ -1241,8 +1241,8 @@ NonNullType : SemanticNonNullType : -- ! NamedType -- ! ListType +- 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 diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index db38f1933..f6f642527 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1915,8 +1915,8 @@ _unless_ accompanied by a _field error_. The Semantic-Non-Null 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 leading exclamation mark is used to denote a -field that uses a Semantic-Non-Null type like this: `name: !String`. +a field error being raised. A trailing asterisk is used to denote a field that +uses a Semantic-Non-Null type like this: `name: String*`. Semantic-Non-Null types are only valid for use as an _output type_; they must not be used as an _input type_. @@ -1955,36 +1955,36 @@ The List and Semantic-Non-Null wrapping types can compose, representing more complex types. The rules for result coercion of Lists and Semantic-Non-Null types apply in a recursive fashion. -For example if the inner item type of a List is Semantic-Non-Null (e.g. `[!T]`), +For example if the inner item type of a List is Semantic-Non-Null (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 Semantic-Non-Null is a List (e.g. -`![T]`), then {null} is not accepted without an accompanying field error being +`[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) | +| `[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 diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index be97fdc2e..b00205cee 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -461,7 +461,7 @@ RecursivelyStripSemanticNonNullTypes(type): - Otherwise, return {type}. Note: This algorithm recursively removes all Semantic-Non-Null type wrappers -(e.g. `![[!Int]!]` would become `[[Int]!]`). This is to support legacy clients: +(e.g. `[[Int*]!]*` would become `[[Int]!]`). This is to support legacy clients: they can safely treat a Semantic-Non-Null type as the underlying nullable type. ### The \_\_InputValue Type From 90c668c658cb1b67b61144bbf4df1c04e240c447 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 10 Mar 2025 11:32:44 +0000 Subject: [PATCH 09/10] Fix bug in RecursivelyStripSemanticNonNullTypes --- spec/Section 4 -- Introspection.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index b00205cee..19d8d0afd 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -458,6 +458,8 @@ RecursivelyStripSemanticNonNullTypes(type): - If {type} is a Semantic-Non-Null type: - Let {innerType} be the inner type of {type}. - Return {RecursivelyStripSemanticNonNullTypes(innerType)}. +- Otherwise, if {type} is a Non-Null type or List type: + - Return {RecursivelyStripSemanticNonNullTypes(type)}. - Otherwise, return {type}. Note: This algorithm recursively removes all Semantic-Non-Null type wrappers From 5e6ebb2b6e7d98a5ae9979c0a6a2f4086485ccdd Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 10 Mar 2025 11:42:21 +0000 Subject: [PATCH 10/10] Fix formatting --- spec/Section 3 -- Type System.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 581a63ffa..9a5a936ae 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -353,7 +353,7 @@ types may only be used as output types. IsInputType(type): - If {type} is a Semantic-Non-Null type: - - Return {false} + - Return {false}. - If {type} is a List type or Non-Null type: - Let {unwrappedType} be the unwrapped type of {type}. - Return IsInputType({unwrappedType}).