Skip to main content

List coercion algorithm

At a glance

Timeline


Fixes #1002.

Previously, list coercion does not detail what to do with variables at all, and that could lead to either a null pointer exception, or to double-coercion of the variable value if you're only following the spec.

Consider the following valid schema:

type Query {
sum(numbers:[Int!]!): Int
}

and the query that is valid against this schema:

query Q ($number: Int = 3) {
sum(numbers: [1, $number, 3])
}

NOTE: We're using the variable in a list item position!

If you issue this to the GraphQL server with variables {"number": null} then CoerceVariableValues will give you {"number": null} and when you fast-forward to CoerceArgumentValues you'll go in to 5.j.iii.1:

https://spec.graphql.org/draft/#sel-NANTHHCJFTDFBBCAACGB0yS

  • Let {coercedValues} be an empty unordered Map. coercedValues = {}
  • Let {argumentValues} be the argument values provided in {field}. argumentValues = { numbers: [1, $number, 3] }
  • Let {fieldName} be the name of {field}. fieldName = 'sum'
  • Let {argumentDefinitions} be the arguments defined by {objectType} for the field named {fieldName}. argumentDefinitions = { numbers: ... }
  • For each {argumentDefinition} in {argumentDefinitions}:
    • Let {argumentName} be the name of {argumentDefinition}. argumentName = 'numbers'
    • Let {argumentType} be the expected type of {argumentDefinition}. argumentType = [Int!]!
    • Let {defaultValue} be the default value for {argumentDefinition}. defaultValue = undefined
    • Let {hasValue} be {true} if {argumentValues} provides a value for the name {argumentName}. hasValue = true
    • Let {argumentValue} be the value provided in {argumentValues} for the name {argumentName}. argumentValue = [1, $number, 3]
    • If {argumentValue} is a {Variable}: NOPE
      • Let {variableName} be the name of {argumentValue}.
      • Let {hasValue} be {true} if {variableValues} provides a value for the name {variableName}.
      • Let {value} be the value provided in {variableValues} for the name {variableName}.
    • Otherwise, let​ {value} be {argumentValue}. value = [1, $number, 3]
    • If {hasValue} is not {true} and {defaultValue} exists (including {null}): NOT TRIGGERED
      • Add an entry to {coercedValues} named {argumentName} with the value {defaultValue}.
    • Otherwise if {argumentType} is a Non-Nullable type, and either {hasValue} is not {true} or {value} is {null}, raise a field error. NOT TRIGGERED
    • Otherwise if {hasValue} is true: Yes, it is
      • If {value} is {null}: It is not, it is a list
        • Add an entry to {coercedValues} named {argumentName} with the value {null}.
      • Otherwise, if {argumentValue} is a {Variable}: It is not, it is a list
        • Add an entry to {coercedValues} named {argumentName} with the value {value}.
      • Otherwise: YES
        • If {value} cannot be coerced according to the input coercion rules of {argumentType}, raise a field error. TIME TO VISIT LIST COERCION
        • Let {coercedValue} be the result of coercing {value} according to the input coercion rules of {argumentType}.
        • Add an entry to {coercedValues} named {argumentName} with the value {coercedValue}.
  • Return {coercedValues}.

Time to visit list coercion

We need to coerce the value [1, $number, 3] to the non-nullable type [Int!]!.

Step 1: handle the non-null. It's not null. Great!

Now we need to coerce the value [1, $number, 3] to the list type [Int!].

Here's what the spec says about input coercion for lists:

When expected as an input, list values are accepted only when each item in the list can be accepted by the list’s item type.

If the value passed as an input to a list type is not a list and not the null value, then the result of input coercion is a list of size one, where the single item value is the result of input coercion for the list’s item type on the provided value (note this may apply recursively for nested lists).

This allows inputs which accept one or many arguments (sometimes referred to as “var​ args”) to declare their input type as a list while for the common case of a single value, a client can just pass that value directly rather than constructing the list.

We have a list, so we only care about the bold line.

This line seems to miss a bunch of situations.

For example: if we were coercing to [Int] the value [1, $number, 3] with variables {} then is $number (which is undefined, since it wasn't provided in the variables) "accepted by the list's item type"? Really we must coerce this to null, but that doesn't seem to be detailed. In fact this entire section doesn't mention variables at all.

We're actually coercing to [Int!], so the question is: is $number accepted by the list's item type? $number itself is a variable, so...


I've attempted to solve this problem by being much more explicit about the input coercion for lists, inspired by the input coercion for input objects. I've also added a non-normative note highlighting the risk of a null variable being fed through into a non-nullable position, why that can occur (validation) and what we do about it (field error). I've also expanded the table with both variables and many more examples to cover many more edge cases.