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: SemanticNonNull type (null only on error) #1065

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions spec/Section 2 -- Language.md
Expand Up @@ -1239,6 +1239,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.
Expand Down
93 changes: 89 additions & 4 deletions spec/Section 3 -- Type System.md
Expand Up @@ -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):

Expand Down Expand Up @@ -1859,6 +1866,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

Expand Down Expand Up @@ -1892,6 +1900,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 |

## 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`.

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_ an error has been logged for 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?
Expand Down
44 changes: 41 additions & 3 deletions spec/Section 4 -- Introspection.md
Expand Up @@ -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
}
Expand Down Expand Up @@ -263,6 +264,7 @@ possible value of the `__TypeKind` enum:
- {"INPUT_OBJECT"}
- {"LIST"}
- {"NON_NULL"}
- {"SEMANTIC_NON_NULL"}

**Scalar**

Expand Down Expand Up @@ -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 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.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
Expand All @@ -422,10 +445,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 `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` optionally provides a reason why this field is deprecated.

RecursivelyStripSemanticNonNullTypes(type):

- If {type} is a Semantic-Non-Null type:
- Let {innerType} be the inner type of {type}.
- Return {RecursivelyStripSemanticNonNullTypes(innerType)}.
- 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
Expand Down
5 changes: 4 additions & 1 deletion spec/Section 6 -- Execution.md
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 Semantic-Non-Null 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 `Semantic-Non-Null` types do not
propagate.