graphql-js icon indicating copy to clipboard operation
graphql-js copied to clipboard

Custom scalars should allow variables in list literals

Open yuchenshi opened this issue 2 weeks ago • 4 comments

Given this file repro.mjs:

import { buildSchema, validateSchema, parse, validate } from "graphql";

const schema = buildSchema(`
scalar GeoPoint
scalar Float

type Query {
  restaurantsNear(loc: GeoPoint!): [String]
}
`)

const schemaErrors = validateSchema(schema);
if (schemaErrors.length > 0) {
  console.dir(schemaErrors);
  throw new Error("Invalid schema");
}

const doc = parse(`
query ($lat: Float!) {
  works: restaurantsNear(loc: [1, 2])
  breaks: restaurantsNear(loc: [$lat, 2])    # <---- Validation error here
  alsoWorks: restaurantsNear(loc: {_wrapper: [$lat, 2]})
}
`)

const errors = validate(schema, doc);
if (errors.length > 0) {
  console.dir(errors);
  throw new Error("validate failed");
}

Run with npm i graphql && node repro.mjs. (Credits to @benjie for the idea of using a .mjs file for a self-contained repro)

Expected: no errors in all three fields Actual: The second field named breaks causes a validation error:

[
  GraphQLError: Variable "$lat" of type "Float!" used in position expecting type "GeoPoint".
      at Object.leave (/path/to/project/node_modules/graphql/validation/rules/VariablesInAllowedPositionRule.js:64:17)
      at Object.leave (/path/to/project/node_modules/graphql/language/visitor.js:321:32)
      at Object.leave (/path/to/project/node_modules/graphql/utilities/TypeInfo.js:411:21)
      at visit (/path/to/project/node_modules/graphql/language/visitor.js:194:21)
      at validate (/path/to/project/node_modules/graphql/validation/validate.js:91:24)
      at file:///path/to/project/load-standalone.mjs:26:16
      at ModuleJob.run (node:internal/modules/esm/module_job:377:25)
      at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:691:26)
      at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
    path: undefined,
    locations: [ [Object], [Object] ],
    extensions: [Object: null prototype] {}
  }
]

Use case

I want to define a query where part of the scalar is provided by user input and the other half is fixed (using trusted documents). The lat/lng use case is just an example. If it's not convincing enough, consider other scalars such as Range ([low, high], where the user should only be able modify, say, low) or Vector. Or even Raw / JSON.

I looked at the current draft spec (re: VariablesInAllowedPositionRule) and it does not seem to forbid variables within custom scalars. I also looked at the proposed clarification https://github.com/graphql/graphql-spec/pull/1156 and what I'm trying to do isn't against the spirit. Note that while (loc: [$lat, 2]) does not work, (loc: {_wrapper: [$lat, 2]}) works. I don't see adding a dummy object wrapper changes the interpretation of the spec here. So I believe it's just a bug / limitation in graphql-js that

BTW, this also happens in the GraphQL LSP, which I believe share the same validation rules.

(I can work around it by adding an alternative serialization format to my scalar: {_wrapper: [the actual array here]} but it is a hack. Especially, according to GraphQL custom scalar implementation guide, I also need to support it as a serialization format too for compliance. I don't want to commit to supporting this wrapper format as part of my API.)

yuchenshi avatar Nov 26 '25 02:11 yuchenshi