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 c2c601fa2..f0e1245f2 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 @@ -1243,6 +1243,11 @@ NonNullType : - NamedType ! - ListType ! +SemanticNonNullType : + +- 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 04e4fa450..9a5a936ae 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: @@ -953,16 +958,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 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)}. -2. If {fieldType} is a List type and {implementedFieldType} is also a List type: +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): @@ -1861,6 +1873,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 Semantic-Non-Null type. ### Combining List and Non-Null @@ -1894,6 +1907,87 @@ 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 | +## Semantic-Non-Null + +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 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_. + +**Nullable vs. Optional** + +Fields that return Semantic-Non-Null types will never return the value {null} if +queried _unless_ a _field error_ is associated with that field. + +**Result Coercion** + +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 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 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 +section. + +**Input Coercion** + +Semantic-Non-Null types are never valid inputs. + +**Type Validation** + +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 Semantic-Non-Null + +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*]`), +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) | + ## Directives DirectiveDefinition : Description? directive @ Name ArgumentsDefinition? diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index b260559cb..2680186b5 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -162,13 +162,14 @@ enum __TypeKind { INPUT_OBJECT LIST NON_NULL + SEMANTIC_NON_NULL } type __Field { name: String! description: String args(includeDeprecated: Boolean = false): [__InputValue!]! - type: __Type! + type(includeSemanticNonNull: Boolean! = false): __Type! isDeprecated: Boolean! deprecationReason: String } @@ -263,6 +264,7 @@ possible value of the `__TypeKind` enum: - {"INPUT_OBJECT"} - {"LIST"} - {"NON_NULL"} +- {"SEMANTIC_NON_NULL"} **Scalar** @@ -400,12 +402,33 @@ 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 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. +- `ofType` must return a type of any kind except Non-Null and Semantic-Non-Null. +- All other fields must return {null}. + +**Semantic-Non-Null** + +GraphQL types are nullable. The value {null} is a valid response for field type. + +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 Semantic-Non-Null of Lists. However it must not +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. + +Fields\: + +- `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 @@ -422,11 +445,28 @@ 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 `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 + {RecursivelyStripSemanticNonNullTypes(fieldType)}. - `isDeprecated` returns {true} if this field should no longer be used, otherwise {false}. - `deprecationReason` returns the reason why this field is deprecated, or null if this field is not deprecated. +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 +(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 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 d88c685f9..e96463be4 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -700,7 +700,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 Semantic-Non-Null type: - Let {innerType} be the inner type of {fieldType}. - Let {completedResult} be the result of calling {CompleteValue(innerType, fields, result, variableValues)}. @@ -835,3 +835,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 `Semantic-Non-Null` types do not +propagate.