ruby-grape/grape · error · ArgumentError

:type may not be supplied with :types

Error message

:type may not be supplied with :types

What it means

`type:` declares a single coercion target; `types:` declares a union of allowed types. They are mutually exclusive — a param cannot be coerced to exactly one type and to several types at once — so `ValidationsSpec#initialize` raises ArgumentError the moment a `requires`/`optional` declaration carries both keys.

Source

Thrown at lib/grape/validations/validations_spec.rb:43

      # Keys consumed by the spec itself; must NOT be dispatched as validators
      # by the caller. Documentation-only keys are filtered through a separate
      # set so that dual-purpose keys (length, default, values, except_values)
      # aren't accidentally swallowed.
      SPEC_CONSUMED_KEYS = %i[
        type types coerce coerce_with coerce_message
        presence message
        fail_fast
        desc description documentation
      ].freeze

      attr_reader :raw, :coerce_type, :coerce_method, :coerce_message, :presence_options, :values, :except_values, :default, :allow_blank, :fail_fast, :shared_opts, :validator_entries

      def self.from(validations)
        new(validations)
      end

      def initialize(raw)
        raise ArgumentError, ':type may not be supplied with :types' if raw.key?(:type) && raw.key?(:types)

        @raw = raw
        @coerce_type, @coerce_message, @coerce_method = parse_coerce(raw)
        @values = resolve_value(raw[:values])
        @except_values = resolve_value(raw[:except_values])
        @default = raw[:default]
        @presence_options = raw[:presence]
        @allow_blank = resolve_value(raw[:allow_blank])
        @fail_fast = raw[:fail_fast] || false

        @guessed_coerce_type = guess_coerce_type(declared_coerce_type, @values, @except_values)
        @shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
        @validator_entries = build_validator_entries(raw)

        validate!

        freeze
      end

View on GitHub (pinned to 22d7975629)

Solutions

  1. Delete `:type` and keep `types:` for union declarations
  2. When merging defaults, strip the stale key: `decl = defaults.merge(decl); decl.except!(:type) if decl.key?(:types)`
  3. Mount every endpoint in a spec so load-time declaration errors surface in CI rather than at production boot

Example fix

# before
requires :id, type: Integer, types: [Integer, String]

# after
requires :id, types: [Integer, String]
Defensive patterns

Strategy: validation

Validate before calling

def assert_no_type_and_types!(opts)
  return unless opts.key?(:type) && opts.key?(:types)

  raise ArgumentError, ':type and :types are mutually exclusive (got both in ' + opts.inspect + ')'
end

Try / catch

begin
  requires :id, **merged_opts
rescue ArgumentError => e
  raise "declaration carries both :type and :types after merging defaults: #{e.message}"
end

Prevention

When it happens

Trigger: `requires :id, type: Integer, types: [Integer, String]`; merging a shared defaults hash containing `type:` into a declaration that adds `types:`; generated declarations where both keys slip in.

Common situations: Refactoring a single-typed param into a union and forgetting to delete `type:`; `DEFAULT_OPTS = { type: String }.freeze` merged per-param with a declaration that also passes `types:`.

Related errors


AI-assisted analysis of ruby-grape/grape@22d7975629 (2026-08-21). Data as JSON: /api/errors/90ce2aa6580ec367. Report an issue: GitHub.