instructure/canvas-lms · error · GraphQL::CoercionError

# is not a valid StringMap

Error message

#{input_value.inspect} is not a valid StringMap

What it means

StringMapType is a scalar whose coerce_input validates that the value is a Hash of String keys to String values (ActionController::Parameters are first converted to a plain Hash). Anything else — nested hashes, non-string values, arrays, nil — raises GraphQL::CoercionError with this message naming the offending value.

Solutions

  1. Stringify every key and value before sending: Object.entries(obj) mapped to String(v).
  2. Reject or coerce null/undefined/numeric/boolean values client-side; omit keys whose values are null.
  3. If you need structured (non-string) data, use a different argument type — StringMap is flat strings only.
  4. Flatten nested objects to string values (e.g. JSON.stringify the sub-object as the value).

Example fix

// before
variables: { metadata: { attempts: 3, notes: null } }
// after
variables: { metadata: { attempts: "3", notes: "" } } // or omit null keys
Defensive patterns

Strategy: validation

Validate before calling

function isStringMap(v) {
  if (v == null || typeof v !== 'object' || Array.isArray(v)) return false;
  return Object.entries(v).every(([k, val]) => typeof k === 'string' && typeof val === 'string');
}
if (!isStringMap(vars.metadata)) throw new Error('metadata must be a flat String->String map');

Type guard

const isStringMap = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && Object.values(v).every(x => typeof x === 'string') && Object.keys(v).every(k => typeof k === 'string');

Try / catch

try { await gql(MUTATION, vars); } catch (e) { if (e.message.includes('is not a valid StringMap')) { vars = stringifyMapShallow(vars.metadata); return retry(vars); } throw e; }

Prevention

When it happens

Trigger: Passing a variable with non-string values (numbers, booleans, nulls) or nested objects into a StringMap-typed argument; sending an array instead of an object; sending a multipart/GraphQL-upload parameter that remains an unconvertible type.

Common situations: JS clients stringify JSON in variables but forget to coerce numeric fields (e.g. {"count": 5}); building the map from parsed form data that kept integer values; locale-specific metadata fields where one value is null.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/98c009c62f68b5d2. Report an issue: GitHub.

Appendix: source

Thrown at app/graphql/types/string_map_type.rb:33

# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

module Types
  class StringMapType < Types::BaseScalar
    description "A hash with string keys and string values"

    def self.coerce_input(input_value, _context)
      return nil if input_value.nil?

      if input_value.is_a?(ActionController::Parameters)
        input_value = input_value.to_unsafe_h
      end

      unless input_value.is_a?(Hash) && input_value.all? { |k, v| k.is_a?(String) && v.is_a?(String) }
        raise GraphQL::CoercionError, "#{input_value.inspect} is not a valid StringMap"
      end

      input_value
    end

    def self.coerce_result(ruby_value, _context)
      unless ruby_value.is_a?(Hash) && ruby_value.all? { |k, v| (k.is_a?(Symbol) || k.is_a?(String)) && v.is_a?(String) }
        raise GraphQL::CoercionError, "#{ruby_value.inspect} is not a valid StringMap"
      end

      ruby_value.transform_keys(&:to_s)
    end
  end
end

View on GitHub (pinned to 1c9f0bb801)