-
-
Notifications
You must be signed in to change notification settings - Fork 262
Bugfix/validation/3.x #2847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: series/3.x
Are you sure you want to change the base?
Bugfix/validation/3.x #2847
Conversation
li-nkSN
commented
Jan 2, 2026
- "Stop coercing variables into operations they never signed up for"
- "Variables have commitment issues - only validate the operation they're with"
- "Teach VariablesCoercer to respect boundaries (operationName ones)"
- "End variable peer pressure: don't validate across operation lines"
- "VariablesCoercer was being too clingy with other operations' variables"
- "Scope the coercion: variables shouldn't answer for operations they're not in"
- "$token: 'I'm with VerifyToken, stop asking me about SendEmail!'"
| object VariablesCoercer { | ||
| import caliban.validation.ValidationOps._ | ||
|
|
||
| def coerceVariables( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that function isn't used anywhere so we can delete it since 3.x is not released yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you didn't address this one
| .map(_.variableDefinitions) | ||
| .getOrElse(Nil) | ||
| case None => | ||
| doc.operationDefinitions match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems we don't need the pattern matching here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one too
| operations.flatMap(_.variableDefinitions.map(d => d.name -> d)).toMap | ||
|
|
||
| /** Get only the operations that should be validated based on operationName */ | ||
| def selectedOperations: List[OperationDefinition] = operationName match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this method is not needed (see @ghostdogpr comment) but if for some reason it is needed I wouldn't add it to Context directly as we might accidentally use it over operations somewhere.
| selectionSets: List[Selection], | ||
| variables: Map[String, InputValue] | ||
| variables: Map[String, InputValue], | ||
| operationName: Option[String] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you follow my comment below, then this is no longer used anywhere. In fact, you could keep everything binary-compatible by removing this argument
| variables: Map[String, InputValue], | ||
| validations: List[QueryValidation] | ||
| validations: List[QueryValidation], | ||
| operationName: Option[String] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also not needed if we were to remove operationName from Context
|
Thank you both for the detailed review! I've addressed the feedback and added additional tests to demonstrate the full scope of the issue. Accepted Changes (pushed)I've incorporated all the style suggestions:
Regarding Field Validation Scope@ghostdogpr - I understand your concern that the spec may require coercion only for the requested operation while validation covers all operations. However, I've added end-to-end tests that demonstrate why both changes are necessary for correct behavior. The ProblemWith only the to: Both errors prevent executing New E2E TestsI've added a test suite that exercises the full interpreter execution path ( suite("End-to-end execution with multi-operation documents")(
test("executing SendEmail with only $email should succeed") {
for {
interpreter <- api.interpreter
result <- interpreter.execute(
multiOperationDocument,
operationName = Some("SendEmail"),
variables = Map("email" -> StringValue("test@example.com"))
)
} yield assertTrue(result.errors.isEmpty)
},
// ... symmetrical test for VerifyToken
// ... tests confirming required variables are still validated for selected operation
)These tests pass only when both fixes are applied. They fail without the Spec InterpretationThe spec states:
If we're only executing one operation, validating fields that reference variables from non-executed operations seems inconsistent with the per-operation scoping principle. Real-World ImpactThis is a common scenario with GraphQL code generators (Ferry, Apollo, etc.) that bundle multiple operations into single documents. Users should be able to execute any individual operation without providing variables for unrelated operations in the same document. Regarding
|
|
@li-nkSN I disagree with your interpretation of the rule. The spec says clearly that all operations should be validated (all validation algorithms start with The failing validation is https://spec.graphql.org/September2025/#sec-Values-of-Correct-Type and there is this note:
It is basically saying that we should only check that the variable has the right type but not check its value at this point, since the value will be checked during the coercing step. We are currently validating it twice. So I think we could remove the validation here: https://github.com/ghostdogpr/caliban/blob/f6f9feda93e7c99ae4d05c4395fad364ccfae369/core/src/main/scala/caliban/validation/ValueValidator.scala#L52C15-L52C28 |
2ae1cde to
55f07cf
Compare
@ghostdogpr : Thank you for the clarification. You're right - I misread the spec regarding validation scope. Your analysis of the duplicate value checking makes sense. I updated the PR based on this feedback. Does this seem appropriate? |
|
Implemented your suggested fix in ValueValidator.scala. As expected, this changes when variable value errors are detected - they now surface as ExecutionErrors during coercion rather than ValidationErrors during document validation. This causes 3 tests to fail:
These tests expect ValidationError but now receive ExecutionError with the same underlying issue detected. Then I will work on updating the tests and make another commit. |
| object VariablesCoercer { | ||
| import caliban.validation.ValidationOps._ | ||
|
|
||
| def coerceVariables( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you didn't address this one
| .map(_.variableDefinitions) | ||
| .getOrElse(Nil) | ||
| case None => | ||
| doc.operationDefinitions match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one too
| "Input field was null but was supposed to be non-null." | ||
| ) | ||
| ) | ||
| // Variable value validation now happens at coercion/execution time, not validation time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we keep comparing precise error messages? contains("null") isn't great. Same for the other tests