Skip to content

Commit 0356f0c

Browse files
committed
RFC: Fix ambiguity with null variable values and default values (#418)
This change corresponds to a spec proposal (#418) which solves an ambiguity in how variable values and default values behave with explicit null values, and changes validation rules to better define the behavior of default values. Otherwise, this ambiguity can allows for null values to appear in non-null argument values, which may result in unforeseen null-pointer-errors. In summary this change includes: * Removal of `VariablesDefaultValueAllowed` validation rule. All variables may now specify a default value. * Change to `VariablesInAllowedPosition` rule to explicitly not allow a `null` default value when flowing into a non-null argument, and now allows optional (nullable) variables in non-null arguments that provide default values. * Changes to `ProvidedRequiredArguments` rule (renamed from `ProvidedNonNullArguments`) to no longer require values to be provided to non-null arguments which provide a default value. * Changes to `getVariableValues()` and `getArgumentValues()` to ensure a `null` value never flows into a non-null argument. * Changes to `valueFromAST()` to ensure `null` variable values do not flow into non-null types. * Adds to the `TypeInfo` API to allow referencing the expected default value at a given AST position.
1 parent 2b2467a commit 0356f0c

File tree

3 files changed

+188
-129
lines changed

3 files changed

+188
-129
lines changed

spec/Section 3 -- Type System.md

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,25 +1263,36 @@ type of an Object or Interface field.
12631263
**Input Coercion**
12641264

12651265
The value for an input object should be an input object literal or an unordered
1266-
map supplied by a variable, otherwise an error should be thrown. In either
1267-
case, the input object literal or unordered map should not contain any entries
1266+
map supplied by a variable, otherwise an error must be thrown. In either
1267+
case, the input object literal or unordered map must not contain any entries
12681268
with names not defined by a field of this input object type, otherwise an error
1269-
should be thrown.
1269+
must be thrown.
12701270

12711271
The result of coercion is an unordered map with an entry for each field both
1272-
defined by the input object type and provided with a value. If the value {null}
1273-
was provided, an entry in the coerced unordered map must exist for that field.
1274-
In other words, there is a semantic difference between the explicitly provided
1275-
value {null} versus having not provided a value.
1276-
1277-
The value of each entry in the coerced unordered map is the result of input
1278-
coercion of the value provided for that field for the type of the field defined
1279-
by the input object type
1280-
1281-
Any non-nullable field defined by the input object type which does not have
1282-
a corresponding entry in the original value, or is represented by a variable
1283-
which was not provided a value, or for which the value {null} was provided, an
1284-
error should be thrown.
1272+
defined by the input object type and for which a value exists. The resulting map
1273+
is constructed with the following rules:
1274+
1275+
* If no value is provided for a defined input object field and that field
1276+
definition provides a default value, the default value should be used. If no
1277+
default value is provided and the input object field's type is non-null, an
1278+
error should be thrown. Otherwise, if the field is not required, then no entry
1279+
is added to the coerced unordered map.
1280+
1281+
* If the value {null} was provided for an input object field, and the field's
1282+
type is not a non-null type, an entry in the coerced unordered map is given
1283+
the value {null}. In other words, there is a semantic difference between the
1284+
explicitly provided value {null} versus having not provided a value.
1285+
1286+
* If a literal value is provided for an input object field, an entry in the
1287+
coerced unordered map is given the result of coercing that value according
1288+
to the input coercion rules for the type of that field.
1289+
1290+
* If a variable is provided for an input object field, the runtime value of that
1291+
variable must be used. If the runtime value is {null} and the field type
1292+
is non-null, a field error must be thrown. If no runtime value is provided,
1293+
the variable definition's default value should be used. If the variable
1294+
definition does not provide a default value, the input object field
1295+
definition's default value should be used.
12851296

12861297
Following are examples of input coercion for an input object type with a
12871298
`String` field `a` and a required (non-null) `Int!` field `b`:

spec/Section 5 -- Validation.md

Lines changed: 109 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ type Arguments {
676676
intArgField(intArg: Int): Int
677677
nonNullBooleanArgField(nonNullBooleanArg: Boolean!): Boolean!
678678
booleanListArgField(booleanListArg: [Boolean]!): [Boolean]
679+
optionalNonNullBooleanArgField(optionalBooleanArg: Boolean! = false): Boolean!
679680
}
680681

681682
extend type Query {
@@ -710,25 +711,25 @@ and invalid.
710711
* {arguments} must be the set containing only {argument}.
711712

712713

713-
#### Required Non-Null Arguments
714+
#### Required Arguments
714715

715716
* For each Field or Directive in the document.
716717
* Let {arguments} be the arguments provided by the Field or Directive.
717718
* Let {argumentDefinitions} be the set of argument definitions of that Field or Directive.
718-
* For each {definition} in {argumentDefinitions}:
719-
* Let {type} be the expected type of {definition}.
720-
* If {type} is Non-Null:
721-
* Let {argumentName} be the name of {definition}.
719+
* For each {argumentDefinition} in {argumentDefinitions}:
720+
* Let {type} be the expected type of {argumentDefinition}.
721+
* Let {defaultValue} be the default value of {argumentDefinition}.
722+
* If {type} is Non-Null and {defaultValue} does not exist:
723+
* Let {argumentName} be the name of {argumentDefinition}.
722724
* Let {argument} be the argument in {arguments} named {argumentName}
723725
* {argument} must exist.
724726
* Let {value} be the value of {argument}.
725727
* {value} must not be the {null} literal.
726728

727729
**Explanatory Text**
728730

729-
Arguments can be required. If the argument type is non-null the argument is
730-
required and furthermore the explicit value {null} may not be provided.
731-
Otherwise, the argument is optional.
731+
Arguments can be required. An argument is required if the argument type is
732+
non-null and does not have a default value. Otherwise, the argument is optional.
732733

733734
For example the following are valid:
734735

@@ -752,19 +753,20 @@ fragment goodBooleanArgDefault on Arguments {
752753
}
753754
```
754755

755-
but this is not valid on a non-null argument.
756+
but this is not valid on a required argument.
756757

757758
```graphql counter-example
758759
fragment missingRequiredArg on Arguments {
759760
nonNullBooleanArgField
760761
}
761762
```
762763

763-
Providing the explicit value {null} is also not valid.
764+
Providing the explicit value {null} is also not valid since required arguments
765+
always have a non-null type.
764766

765767
```graphql counter-example
766768
fragment missingRequiredArg on Arguments {
767-
notNullBooleanArgField(nonNullBooleanArg: null)
769+
nonNullBooleanArgField(nonNullBooleanArg: null)
768770
}
769771
```
770772

@@ -1358,6 +1360,31 @@ For example the following query will not pass validation.
13581360
```
13591361

13601362

1363+
### Input Object Required Fields
1364+
1365+
**Formal Specification**
1366+
1367+
* For each Input Object in the document.
1368+
* Let {fields} be the fields provided by that Input Object.
1369+
* Let {fieldDefinitions} be the set of input field definitions of that Input Object.
1370+
* For each {fieldDefinition} in {fieldDefinitions}:
1371+
* Let {type} be the expected type of {fieldDefinition}.
1372+
* Let {defaultValue} be the default value of {fieldDefinition}.
1373+
* If {type} is Non-Null and {defaultValue} does not exist:
1374+
* Let {fieldName} be the name of {fieldDefinition}.
1375+
* Let {field} be the input field in {fields} named {fieldName}
1376+
* {field} must exist.
1377+
* Let {value} be the value of {field}.
1378+
* {value} must not be the {null} literal.
1379+
1380+
**Explanatory Text**
1381+
1382+
Input object fields may be required. Much like a field may have required
1383+
arguments, an input object may have required fields. An input field is required
1384+
if it has a non-null type and does not have a default value. Otherwise, the
1385+
input object field is optional.
1386+
1387+
13611388
## Directives
13621389

13631390

@@ -1494,44 +1521,6 @@ fragment HouseTrainedFragment {
14941521
```
14951522

14961523

1497-
### Variable Default Value Is Allowed
1498-
1499-
**Formal Specification**
1500-
1501-
* For every Variable Definition {variableDefinition} in a document
1502-
* Let {variableType} be the type of {variableDefinition}
1503-
* Let {defaultValue} be the default value of {variableDefinition}
1504-
* If {variableType} is Non-null:
1505-
* {defaultValue} must be undefined.
1506-
1507-
**Explanatory Text**
1508-
1509-
Variables defined by operations are allowed to define default values
1510-
if the type of that variable is not non-null.
1511-
1512-
For example the following query will pass validation.
1513-
1514-
```graphql example
1515-
query houseTrainedQuery($atOtherHomes: Boolean = true) {
1516-
dog {
1517-
isHousetrained(atOtherHomes: $atOtherHomes)
1518-
}
1519-
}
1520-
```
1521-
1522-
However if the variable is defined as non-null, default values
1523-
are unreachable. Therefore queries such as the following fail
1524-
validation
1525-
1526-
```graphql counter-example
1527-
query houseTrainedQuery($atOtherHomes: Boolean! = true) {
1528-
dog {
1529-
isHousetrained(atOtherHomes: $atOtherHomes)
1530-
}
1531-
}
1532-
```
1533-
1534-
15351524
### Variables Are Input Types
15361525

15371526
**Formal Specification**
@@ -1833,20 +1822,45 @@ an extraneous variable.
18331822

18341823
**Formal Specification**
18351824

1836-
* For each {operation} in {document}
1837-
* Let {variableUsages} be all usages transitively included in the {operation}
1838-
* For each {variableUsage} in {variableUsages}
1839-
* Let {variableType} be the type of variable definition in the {operation}
1840-
* Let {argumentType} be the type of the argument the variable is passed to.
1841-
* Let {hasDefault} be true if the variable definition defines a default.
1842-
* AreTypesCompatible({argumentType}, {variableType}, {hasDefault}) must be true
1843-
1844-
* AreTypesCompatible({argumentType}, {variableType}, {hasDefault}):
1845-
* If {hasDefault} is true, treat the {variableType} as non-null.
1846-
* If inner type of {argumentType} and {variableType} are different, return false
1847-
* If {argumentType} and {variableType} have different list dimensions, return false
1848-
* If any list level of {variableType} is not non-null, and the corresponding level
1849-
in {argument} is non-null, the types are not compatible.
1825+
* For each {operation} in {document}:
1826+
* Let {variableUsages} be all usages transitively included in the {operation}.
1827+
* For each {variableUsage} in {variableUsages}:
1828+
* Let {variableName} be the name of {variableUsage}.
1829+
* Let {variableDefinition} be the {VariableDefinition} named {variableName}
1830+
defined within {operation}.
1831+
* {IsVariableUsageAllowed(variableDefinition, variableUsage)} must be {true}.
1832+
1833+
IsVariableUsageAllowed(variableDefinition, variableUsage):
1834+
* Let {variableType} be the expected type of {variableDefinition}.
1835+
* Let {locationType} be the expected type of the {Argument}, {ObjectField},
1836+
or {ListValue} entry where {variableUsage} is located.
1837+
* If {locationType} is a non-null type AND {variableType} is NOT a non-null type:
1838+
* Let {hasNonNullVariableDefaultValue} be {true} if a default value exists
1839+
for {variableDefinition} and is not the value {null}.
1840+
* Let {hasLocationDefaultValue} be {true} if a default value exists for
1841+
the {Argument} or {ObjectField} where {variableUsage} is located.
1842+
* If {hasNonNullVariableDefaultValue} is NOT {true} AND
1843+
{hasLocationDefaultValue} is NOT {true}, return {false}.
1844+
* Let {nullableLocationType} be the unwrapped nullable type of {locationType}.
1845+
* Return {AreTypesCompatible(variableType, nullableLocationType)}.
1846+
* Return {AreTypesCompatible(variableType, locationType)}.
1847+
1848+
AreTypesCompatible(variableType, locationType):
1849+
* If {locationType} is a non-null type:
1850+
* If {variableType} is NOT a non-null type, return {false}.
1851+
* Let {nullableLocationType} be the unwrapped nullable type of {locationType}.
1852+
* Let {nullableVariableType} be the unwrapped nullable type of {variableType}.
1853+
* Return {AreTypesCompatible(nullableVariableType, nullableLocationType)}.
1854+
* Otherwise, if {variableType} is a non-null type:
1855+
* Let {nullableVariableType} be the nullable type of {variableType}.
1856+
* Return {AreTypesCompatible(nullableVariableType, locationType)}.
1857+
* Otherwise, if {locationType} is a list type:
1858+
* If {variableType} is NOT a list type, return {false}.
1859+
* Let {itemLocationType} be the unwrapped item type of {locationType}.
1860+
* Let {itemVariableType} be the unwrapped item type of {variableType}.
1861+
* Return {AreTypesCompatible(itemVariableType, itemLocationType)}.
1862+
* Otherwise, if {variableType} is a list type, return {false}.
1863+
* Return {true} if {variableType} and {locationType} are identical, otherwise {false}.
18501864

18511865
**Explanatory Text**
18521866

@@ -1890,17 +1904,6 @@ query booleanArgQuery($booleanArg: Boolean) {
18901904
}
18911905
```
18921906

1893-
A notable exception is when default arguments are provided. They are, in effect,
1894-
treated as non-nulls.
1895-
1896-
```graphql example
1897-
query booleanArgQueryWithDefault($booleanArg: Boolean = true) {
1898-
arguments {
1899-
nonNullBooleanArgField(nonNullBooleanArg: $booleanArg)
1900-
}
1901-
}
1902-
```
1903-
19041907
For list types, the same rules around nullability apply to both outer types
19051908
and inner types. A nullable list cannot be passed to a non-null list, and a list
19061909
of nullable values cannot be passed to a list of non-null values.
@@ -1925,5 +1928,36 @@ query listToNonNullList($booleanList: [Boolean]) {
19251928
```
19261929

19271930
This would fail validation because a `[T]` cannot be passed to a `[T]!`.
1928-
19291931
Similarly a `[T]` cannot be passed to a `[T!]`.
1932+
1933+
**Allowing optional variables when default values exist**
1934+
1935+
A notable exception to typical variable type compatibility is allowing a
1936+
variable definition with a nullable type to be provided to a non-null location
1937+
as long as either that variable or that location provides a default value.
1938+
1939+
```graphql example
1940+
query booleanArgQueryWithDefault($booleanArg: Boolean) {
1941+
arguments {
1942+
optionalNonNullBooleanArgField(optionalBooleanArg: $booleanArg)
1943+
}
1944+
}
1945+
```
1946+
1947+
In the example above, an optional variable is allowed to be used in an non-null argument which provides a default value.
1948+
1949+
```graphql example
1950+
query booleanArgQueryWithDefault($booleanArg: Boolean = true) {
1951+
arguments {
1952+
nonNullBooleanArgField(nonNullBooleanArg: $booleanArg)
1953+
}
1954+
}
1955+
```
1956+
1957+
In the example above, a variable provides a default value and can be used in a
1958+
non-null argument. This behavior is explicitly supported for compatibility with
1959+
earlier editions of this specification. GraphQL authoring tools may wish to
1960+
report this is a warning with the suggestion to replace `Boolean` with `Boolean!`.
1961+
1962+
Note: The value {null} could still be provided to a such a variable at runtime.
1963+
A non-null argument must produce a field error if provided a {null} value.

0 commit comments

Comments
 (0)