Skip to content

Commit 63a508c

Browse files
authored
RFC: Fix ambiguity with null variable values and default values (#418)
* RFC: Fix ambiguity with null variable values and default values > This is a **behavioral change** which changes how explicit `null` values interact with variable and argument default values. This also changes a validation rule which makes the rule more strict. There is currently ambiguity and inconsistency in how `null` values are coerced and resolved as part of variable values, default values, and argument values. This inconsistency and ambiguity can allow for `null` values to appear at non-null arguments, which might result in unforseen null-pointer-errors. This appears in three distinct but related issues: **Validation: All Variable Usages are Allowed** The explicit value `null` may be used as a default value for a variable with a nullable type, however this rule asks to treat a variable's type as non-null if it has a default value. Instead this rule should specifically only treat the variable's type as non-null if the default value is not `null`. Additionally, the `AreTypesCompatible` algorithm is underspecificied, which could lead to further misinterpretation of this validation rule. **Coercing Variable Values** `CoerceVariableValues()` allows the explicit `null` value to be used instead of a default value. This can result in a null value flowing to a non-null argument due to the validation rule mentioned above. Instead a default value must be used even when an explicit `null` value is provided. This is also more consistent with the explanation for validation rule "Variable Default Value Is Allowed" Also, how to treat an explicit `null` value is currently underspecified. While an input object explains that a `null` value should result in an explicit `null` value at the input object field, there is no similar explaination for typical scalar input types. Instead, `CoerceVariableValues()` should explicitly handle the `null` value to make it clear a `null` is the resulting value in the `coercedValues` Map. **Coercing Argument Values** The `CoerceArgumentValues()` algorithm is intentionally similar to `CoerceVariableValues()` and suffers from the same inconsistency. Explicit `null` values should not take precedence over default values, and should also be explicitly handled rather than left to underspecified input scalar coercion. * Updated based on feedback. This updates this proposal to be a bit broader in scope however much narrower in breaking behavior changes. Mirroring the changes in graphql/graphql-js#1274, this update better defines the difference between a "required" and "non-null" argument / input field as a non-null typed argument / input-field with a default value is no longer required. As such the validation rule which prohibited queries from using non-null variables and default values has been removed. This also adds clarity to the input field validation - this rule has existed in the GraphQL.js reference implementation however was found missing within the spec. This also updates the CoerceVariableValues() and CoerceArgumentValues() algorithms to retain explicit null values overriding a default value (minimizing breaking changes), however critically adding additional protection to CoerceArgumentValues() to explicitly block null values from variables - thus allowing the older pattern of passing a nullable variable into a non-null argument while limiting the problematic case of an explicit null value at runtime. * One step further towards the idealized "from scratch" proposal, this makes it more explicitly clear that changing the effective type of a variable definition is only relevent when supporting legacy clients and suggests that new services should not use this behavior. I like that this balances a clear description of how this rule should work for existing services along with a stricter and therefore safer future path for new services. * Editing AreTypesCompatible() to avoid trailing "Otherwise return false" statements for easier reading. Functionality is equivalent. * Update "All Variable Usages are Allowed" to remove breaking change. Also attempts to improve clarity and formatting and adds an example case. * Make related changes to input object coercion rules * Final review edits
1 parent 2b2467a commit 63a508c

File tree

3 files changed

+188
-129
lines changed

3 files changed

+188
-129
lines changed

spec/Section 3 -- Type System.md

+27-16
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

+109-75
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)