Skip to content

Commit ba9b11d

Browse files
committed
[RFC] Null value
This proposal adds a null literal to the GraphQL language and allows it to be provided to nullable typed arguments and input object fields. This presents an opportunity for a GraphQL service to interpret the explicitly provided null differently from the implicitly not-provided value, which may be especially useful when performing mutations using a per-field set API. For example, this query may represent the removal/clearing of the "bar" field from thing with id: 4. ``` mutation editThing { editThing(id: 4, edits: { foo: "added", bar: null }) { # ... } } ``` In addition to allowing `null` as a literal value, this also proposes interpretting the variables JSON to distinguish between explicit null and implicit not provided: ``` mutation editThing($edits: EditObj) { editThing(id: 4, edits: $edits) { # ... } } ``` This variables results in the unsetting of `bar` ``` { "edits": { "foo": "added", "bar": null } } ``` Finally, this proposes following the not-provided-ness of variables to their positions: ``` mutation editThing($editBaz: String) { editThing(id: 4, edits: { foo: "added", bar: null, baz: $editBaz }) { # ... } } ``` Such that the three variables are semantically different: * `{}` The "baz" input field is "not provided" * `{"editBaz": null}` The "baz" input field is `null` * `{"editBaz": "added"}` The "baz" input field is `"added"`
1 parent 7fe69f9 commit ba9b11d

5 files changed

+120
-33
lines changed

spec/Appendix B -- Grammar Summary.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,15 @@ Value[Const] :
129129
- FloatValue
130130
- StringValue
131131
- BooleanValue
132+
- NullValue
132133
- EnumValue
133134
- ListValue[?Const]
134135
- ObjectValue[?Const]
135136

136137
BooleanValue : one of `true` `false`
137138

139+
NullValue : `null`
140+
138141
EnumValue : Name but not `true`, `false` or `null`
139142

140143
ListValue[Const] :

spec/Section 2 -- Language.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ Value[Const] :
581581
- FloatValue
582582
- StringValue
583583
- BooleanValue
584+
- NullValue
584585
- EnumValue
585586
- ListValue[?Const]
586587
- ObjectValue[?Const]
@@ -650,6 +651,31 @@ Strings are lists of characters wrapped in double-quotes `"`. (ex.
650651
`"Hello World"`). White space and other otherwise-ignored characters are
651652
significant within a string value.
652653

654+
#### Null Value
655+
656+
NullValue: `null`
657+
658+
Null values are represented as the keyword `null`.
659+
660+
GraphQL has two similar but not-identical ways to represent the lack of a value,
661+
by explicitly providing the `null` value, or by implicitly not providing a value
662+
at all.
663+
664+
For example, these two field calls are similar, but not identical:
665+
666+
```graphql
667+
{
668+
field(arg: null)
669+
field
670+
}
671+
```
672+
673+
The first has explictly provided `null` to the argument "arg", while the second
674+
has implicitly not provided a value to the argument "arg". A GraphQL service may
675+
interpret the two forms differently, if it is advantageous. For example, to
676+
represent deleting a field vs not altering a field during a
677+
mutation, respectively.
678+
653679
#### Enum Value
654680

655681
EnumValue : Name but not `true`, `false` or `null`
@@ -659,9 +685,6 @@ recommended that Enum values be "all caps". Enum values are only used in
659685
contexts where the precise enumeration type is known. Therefore it's not
660686
necessary to supply an enumeration type name in the literal.
661687

662-
An enum value cannot be "null" in order to avoid confusion. GraphQL
663-
does not supply a value literal to represent the concept {null}.
664-
665688
#### List Value
666689

667690
ListValue[Const] :

spec/Section 3 -- Type System.md

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ and floating-point values, they are interpreted as an integer input value if
113113
they have an empty fractional part (ex. `1.0`) and otherwise as floating-point
114114
input value.
115115

116+
For all types below, with the exception of Non-Null, if the explicit value
117+
`null` is provided, then then the result of input coercion is `null`.
118+
116119
#### Built-in Scalars
117120

118121
GraphQL provides a basic set of well-defined Scalar types. A GraphQL server
@@ -649,14 +652,26 @@ An input object is never a valid result.
649652
**Input Coercion**
650653

651654
The input to an input object should be an unordered map, otherwise an error
652-
should be thrown. The result of the coercion is an unordered map, with an
653-
entry for each input field, whose key is the name of the input field.
654-
The value of an entry in the coerced map is the result of input coercing the
655-
value of the entry in the input with the same key; if the input does not have a
656-
corresponding entry, the value is the result of coercing null. The input
657-
coercion above should be performed according to the input coercion rules of the
655+
should be thrown. This unordered map should not contain any entries with names
656+
not defined by a field of this input object type, otherwise an error should be
657+
thrown.
658+
659+
The result of coercion is an environment-specific unordered map or record struct
660+
defining slots for each field of the input object type.
661+
662+
For each field of the input object type, if the original value has an entry with
663+
the same name, and the value at that entry is a literal value or a variable
664+
which was provided a runtime value, an entry is added to the result with the
665+
name of the field.
666+
667+
The value of that entry in the result is the outcome of input coercing the
668+
original entry value according to the input coercion rules of the
658669
type declared by the input field.
659670

671+
Note: there is a potential semantic difference between the input value
672+
explicitly declaring an input field's value as the value `null` vs having not
673+
declared the input field at all. GraphQL services may optionally interpret these
674+
two cases differently if it is advantageous.
660675

661676
### Lists
662677

@@ -706,40 +721,41 @@ then an error should be raised.
706721

707722
**Input Coercion**
708723

709-
Note that `null` is not a value in GraphQL, so a query cannot look like:
724+
There are a number of ways in which a `null` value may be provided in GraphQL,
725+
all of which must raise an error when provided to a Non-Null type.
726+
727+
If a value is not provided, or the explicit `null` value is provided, an error
728+
must be thrown. Otherwise, the result is the outcome of input coercing the value
729+
according to the input coercion rules of the wrapped type.
730+
731+
The explicit `null` value is not allowed given a non-null input type:
710732

711733
```!graphql
712734
{
713-
field(arg: null)
735+
fieldWithNonNullArg(nonNullArg: null)
714736
}
715737
```
716738

717-
to indicate that the argument is null. Instead, an argument would be null only
718-
if it is omitted:
739+
An omitted argument is not allowed given a non-null input type:
719740

720-
```graphql
741+
```!graphql
721742
{
722-
field
743+
fieldWithNonNullArg
723744
}
724745
```
725746

726-
Or if passed a variable of a nullable type that at runtime was not provided
727-
a value:
747+
A variable of a nullable type that at runtime was not provided a value, or was
748+
provided an explicit `null` value is not allowed given a non-null input type.
728749

729750
```graphql
730751
query withNullableVariable($var: String) {
731-
field(arg: $var)
752+
fieldWithNonNullArg(nonNullArg: $var)
732753
}
733754
```
734755

735-
Hence, if the value for a Non Null type is hard-coded in the query, it is always
736-
coerced using the input coercion for the wrapped type.
756+
Note: the Validation chapter also defines providing a nullable variable type to
757+
a non-null input type as invalid.
737758

738-
When a Non Null input has its value set using a variable, the coerced value
739-
should be `null` if the provided value is `null`-like in the provided
740-
representation, or if the provided value is omitted. Otherwise, the coerced
741-
value is the result of running the wrapped type's input coercion on the
742-
provided value.
743759

744760
## Directives
745761

spec/Section 5 -- Validation.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,11 +518,15 @@ fragment stringIntoInt on Arguments {
518518
* Let {argumentName} be the name of {definition}
519519
* Let {argument} be the argument in {arguments} named {argumentName}
520520
* {argument} must exist.
521+
* Let {value} be the value of {argument}
522+
* {value} must not be the `null` literal.
521523

522524
** Explanatory Text **
523525

524526
Arguments can be required. Arguments are required if the type of the argument
525-
is non-null. If it is not non-null, the argument is optional.
527+
is non-null. If it is not non-null, the argument is optional. When an argument
528+
type is non-null, and is required, the explicit value `null` may also not
529+
be provided.
526530

527531
For example the following are valid:
528532

@@ -554,6 +558,14 @@ fragment missingRequiredArg on Arguments {
554558
}
555559
```
556560

561+
Providing the explicit value `null` is also not valid.
562+
563+
```!graphql
564+
fragment missingRequiredArg on Arguments {
565+
notNullBooleanArgField(nonNullBooleanArg: null)
566+
}
567+
```
568+
557569
## Fragments
558570

559571
### Fragment Declarations

spec/Section 6 -- Execution.md

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,23 @@ GetFieldEntry(objectType, object, fields):
137137
{GetFieldTypeFromObjectType(objectType, firstField)}.
138138
* If {fieldType} is {null}, return {null}, indicating that no entry exists in
139139
the result map.
140-
* Let {resolvedObject} be {ResolveFieldOnObject(objectType, object, fieldEntry)}.
140+
* Let {resolvedObject} be {ResolveFieldOnObject(objectType, object, firstField)}.
141141
* If {resolvedObject} is {null}, return {tuple(responseKey, null)},
142142
indicating that an entry exists in the result map whose value is `null`.
143143
* Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}.
144144
* Let {responseValue} be the result of calling {CompleteValue(fieldType, resolvedObject, subSelectionSet)}.
145145
* Return {tuple(responseKey, responseValue)}.
146146

147-
GetFieldTypeFromObjectType(objectType, firstField):
148-
* Call the method provided by the type system for determining the field type
149-
on a given object type.
147+
GetFieldTypeFromObjectType(objectType, field):
148+
* Let {fieldName} be the name of {field}.
149+
* Return the field type defined by {objectType} with the name {fieldName}.
150150

151-
ResolveFieldOnObject(objectType, object, firstField):
152-
* Call the method provided by the type system for determining the resolution
153-
of a field on a given object.
151+
ResolveFieldOnObject(objectType, object, field):
152+
* Let {fieldName} be the name of {field}.
153+
* Let {arguments} be the result of {ResolveArguments(objectType, field)}
154+
* Call the internal function provided by {objectType} for determining the
155+
resolved value of a field named {fieldName} on a given {object}
156+
provided {arguments}.
154157

155158
MergeSelectionSets(fields):
156159
* Let {selectionSet} be an empty list.
@@ -189,6 +192,36 @@ ResolveAbstractType(abstractType, objectValue):
189192
system for determining the Object type of {abstractType} given the
190193
value {objectValue}.
191194

195+
### Field Arguments
196+
197+
Fields may include arguments which are provided to the underlying runtime in
198+
order to correctly produce a value. These arguments are defined by the field in
199+
the type system to have a specific input type: Scalars, Enum, Input Object, or
200+
List or Non-Null wrapped of these three.
201+
202+
At each argument position in a query may be a literal value or a variable to be
203+
provided at runtime.
204+
205+
ResolveArguments(objectType, field)
206+
* Let {fieldName} be the name of {field}.
207+
* Let {argTypes} be the arguments provided by {objectType} for the field
208+
named {fieldName}.
209+
* Let {argValues} be an empty Map.
210+
* For each {argTypes} as {argName} and {argType}:
211+
* Let {arg} be the argument in {field} named {argName}.
212+
* If {arg} exists:
213+
* Let {value} be the value defined by {arg}.
214+
* If {value} is a Variable:
215+
* If a value was provided for the variable {value}:
216+
* Add an entry to {argValues} named {argName} with the runtime value
217+
of the Variable {value}.
218+
* Else:
219+
* Let {coercedValue} be the result of coercing {value} according to the
220+
input coercion rules of {argType}.
221+
* Add an entry to {argValues} named {argName} with the
222+
value {coercedValue}.
223+
* Return {argValues}.
224+
192225
### Normal evaluation
193226

194227
When evaluating a grouped field set without a serial execution order requirement,

0 commit comments

Comments
 (0)