Skip to content

reject null variable values in non-null list items and input fields - #4507

Merged
andimarek merged 4 commits into
graphql-java:masterfrom
nab-iha3:non-null-variable-list-items
Nov 1, 2026
Merged

andimarek merged 4 commits into
graphql-java:masterfrom
nab-iha3:non-null-variable-list-items

Conversation

@nab-iha3

Copy link
Copy Markdown
Contributor

with type Query { f(arg: [String!]!): String }, a nullable variable with a default is allowed in the non-null list item position (IsVariableUsageAllowed), so this passes validation:

query($v: String = "x") { f(arg: [$v]) }    variables: {"v": null}

data fetcher receives arg = [null], errors = []

ValuesResolverConversion.literalToInternalValueImpl returns coercedVariables.get(name) for a variable reference without looking at the target type, so the explicit null ends up inside a [String!]! list. same inside input objects, {items: [$v]} for items: [String!]. the input object field case, f(arg: {a: $v}) with a: String!, reaches assertShouldNeverHappen instead:

Exception while fetching data (/obj) : Internal error: should never happen: Should have been validated before

both now throw NonNullableValueCoercedAsNullException, which is what the argument level and variables map paths already do. graphql-js valueFromAST rejects the same input (variableValue === null && isNonNullType(type)). the two new ValuesResolverTest cases fail before the change.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 11 6178 (+30 🟢) 6122 (+30 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 17 6178 (+30 🟢) 6121 (+30 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 21 6178 (+30 🟢) 6121 (+30 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 25 6178 (+30 🟢) 6121 (+30 🟢) 0 (±0) 0 (±0) 57 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 24744 (+120 🟢) 24517 (+120 🟢) 0 (±0) 0 (±0) 227 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 30303 3081 90.8% ±0.0%
Branches 8929 1507 85.6% ±0.0%
Methods 8093 1188 87.2% ±0.0%

Changed Class Coverage (1 class)

Class Line Branch Method
g.e.ValuesResolverConversion +0.5% 🟢 +1.0% 🟢 ±0.0%

Full HTML report: build artifact jacoco-html-report

Updated: 2026-11-01 05:18:46 UTC

if (variableValue == null && isNonNull(type)) {
throw new NonNullableValueCoercedAsNullException(type);
}
return variableValue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ye old nullability on variables - this has long been a challenge around is a value present but null versus null because its absent.

I am not sure this is correct or that we can tell here if a value is present and was set explicitly to null versus being null

We may have to use graphql.execution.CoercedVariables#containsKey to distinguish

I would like @andimarek to comment on this PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair concern, but i don't think the distinction changes the outcome at either site.

externalValueToInternalValueForVariables only puts a key into CoercedVariables when the variable was supplied (explicit null included) or has a default, so get() returning null here means either an explicit null or an absent variable with no default. for a non-null target both are invalid: graphql-js valueFromAST rejects a missing variable in a non-null list item the same way it rejects an explicit null. the one place absent has to behave differently is an input field with a default, and that branch already goes through containsKey (hasValue) and runs before the line i changed, so an absent variable still picks up the field default.

i ran the combinations against this branch to check:

query($v: String = "x") { list(arg: [$v]) }            no variables  -> [x]
query($v: String = "x") { list(arg: [$v]) }            {"v": null}   -> Coerced Null value for NonNull type 'String!'
query($v: String) { nullableItems(arg: [$v]) }          no variables  -> [null]  (unchanged)
query($v: String) { nullableItems(arg: [$v]) }          {"v": null}   -> [null]  (unchanged)
query($v: String) { obj(arg: {a: $v}) }   a: String! = "d"   no variables -> [a:d]
query($v: String) { obj(arg: {a: $v}) }   a: String! = "d"   {"v": null}  -> Field 'a' has coerced Null value for NonNull type 'String!'

an absent variable without a default can only reach the list item check with validation bypassed (a nullable variable with no default isn't allowed in a [String!] position, and a non-null variable fails variable coercion first), and rejecting it there still matches graphql-js. happy to switch the list item check to containsKey if you'd rather see the absent case spelled out, but the result would be the same.

@andimarek

Copy link
Copy Markdown
Member

thanks for that @nab-iha3

@andimarek
andimarek enabled auto-merge November 1, 2026 05:13
@andimarek
andimarek merged commit 0267d14 into graphql-java:master Nov 1, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants