Describe the Bug
Any custom GraphQL mutation added via graphQL.mutations in payload.config.ts fails with Cannot convert undefined or null to object when its arguments are supplied via standard GraphQL variables — but succeeds when the exact same values are inlined as literals in the query string. The resolver function is never invoked in the failing case (confirmed via logging inside the resolver), so the failure happens during GraphQL's own argument/variable validation, before any application code runs.
This only reproduces when graphql is resolved to 17.0.2. Pinning the app's graphql dependency to 16.14.2 (while leaving payload/@payloadcms/* unchanged) fully resolves it.
Link to the code that reproduces this issue
https://github.com/tidart/payload-graphql17-repro
Reproduction Steps
Minimal custom mutation (shape doesn't matter — reproduced with two unrelated ones: a redirect-tracking mutation and an unrelated batch-upsert mutation, both failed identically):
graphQL: {
mutations: (graphQL) => ({
recordThing: {
type: new graphQL.GraphQLObjectType({ name: 'Result', fields: { recorded: { type: graphQL.GraphQLBoolean } } }),
args: {
from: { type: new graphQL.GraphQLNonNull(graphQL.GraphQLString) },
to: { type: new graphQL.GraphQLNonNull(graphQL.GraphQLString) },
},
resolve: async () => ({ recorded: true }),
},
}),
},
Query via variables (fails):
{
"query": "mutation($from: String!, $to: String!) { recordThing(from: $from, to: $to) { recorded } }",
"variables": { "from": "/a", "to": "/b" }
}
→ [{"message":"Cannot convert undefined or null to object"}]
Same query, values inlined (succeeds):
{ "query": "mutation { recordThing(from: \"/a\", to: \"/b\") { recorded } }" }
→ {"data":{"recordThing":{"recorded":true}}}
Suspected Root Cause
graphql@17.0.2 ships a conditional exports map (require/node → index.js, import/module → index.mjs), unlike 16.x which has no exports field and resolves to a single file regardless of require vs import. If the app's bundle resolves graphql through both conditions (e.g. Payload's compiled code via require, ESM-first transitive deps like @graphql-tools/* via import), two independent module instances exist simultaneously, each with its own GraphQLNonNull/GraphQLString classes. graphql-js's variable-type validation (comparing a variable's declared type against the field argument's type) relies on instanceof-style type guards, which silently fail across the instance boundary — but only for variables; literal values are coerced directly against the argument's own type object with no cross-instance comparison.
This looks related to the dual-package hazard already discussed in #8386, surfacing through a different graphql-js code path (there: scalar serialization across collection types; here: variable validation for custom mutations).
Which area(s) are affected?
area: graphql
Environment Info
payload: 3.87.1 / 3.90.2 (reproduced on both)
@payloadcms/graphql: 3.87.1
graphql: 17.0.2 (fails) / 16.14.2 (works)
Note: @payloadcms/graphql's own peerDependencies still declare "graphql": "^16.8.1" — including on the latest 4.0.0-internal.* prerelease — so this may be an intentionally-unsupported combination rather than a true bug. Filing in case it's useful tracking toward eventual v17 support, since the symptom is confusing enough (a generic, resolver-bypassing error) to cost real debugging time for anyone who ends up on graphql 17 via a transitive bump.
Describe the Bug
Any custom GraphQL mutation added via graphQL.mutations in payload.config.ts fails with Cannot convert undefined or null to object when its arguments are supplied via standard GraphQL variables — but succeeds when the exact same values are inlined as literals in the query string. The resolver function is never invoked in the failing case (confirmed via logging inside the resolver), so the failure happens during GraphQL's own argument/variable validation, before any application code runs.
This only reproduces when graphql is resolved to 17.0.2. Pinning the app's graphql dependency to 16.14.2 (while leaving payload/@payloadcms/* unchanged) fully resolves it.
Link to the code that reproduces this issue
https://github.com/tidart/payload-graphql17-repro
Reproduction Steps
Minimal custom mutation (shape doesn't matter — reproduced with two unrelated ones: a redirect-tracking mutation and an unrelated batch-upsert mutation, both failed identically):
Query via variables (fails):
Same query, values inlined (succeeds):
Suspected Root Cause
graphql@17.0.2 ships a conditional exports map (require/node → index.js, import/module → index.mjs), unlike 16.x which has no exports field and resolves to a single file regardless of require vs import. If the app's bundle resolves graphql through both conditions (e.g. Payload's compiled code via require, ESM-first transitive deps like @graphql-tools/* via import), two independent module instances exist simultaneously, each with its own GraphQLNonNull/GraphQLString classes. graphql-js's variable-type validation (comparing a variable's declared type against the field argument's type) relies on instanceof-style type guards, which silently fail across the instance boundary — but only for variables; literal values are coerced directly against the argument's own type object with no cross-instance comparison.
This looks related to the dual-package hazard already discussed in #8386, surfacing through a different graphql-js code path (there: scalar serialization across collection types; here: variable validation for custom mutations).
Which area(s) are affected?
area: graphql
Environment Info