ruby-grape/grape · error · ArgumentError

oneof: each variant must be a Proc

Error message

oneof: each variant must be a Proc

What it means

Each `oneof:` variant must be a Proc because Grape evaluates it in a scratch ParamsScope (OneofCollector) to capture its validators. Symbols, strings, hashes, or method names cannot be evaluated that way, so `variants.all?(Proc)` fails and ArgumentError is raised at definition time. Use `types:` for plain type unions.

Source

Thrown at lib/grape/validations/params_scope.rb:408

        # here is correct — the real mounted instance will replay this step
        # with the actual type value.
        return unless coerce_options.type

        validate('coerce', coerce_options, attrs, spec.required?, spec.shared_opts)
      end

      # Translate a `oneof: [proc, proc, ...]` declaration into a list of
      # captured validator arrays — one array per variant. Each variant's
      # block is evaluated in its own +ParamsScope+ backed by an
      # {OneofCollector} so the full params DSL is available inside variants
      # and the resulting validators are kept out of the real API's
      # registration list.
      def process_oneof!(validations)
        raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash

        variants = validations[:oneof]
        raise ArgumentError, 'oneof: must be a non-empty Array of blocks' unless variants.is_a?(Array) && variants.any?
        raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)

        validations[:oneof] = variants.map { |block| OneofCollector.collect(block) }
      end

      def validate(type, options, attrs, required, opts)
        validator_class = Validations.require_validator(type)
        validator_instance = validator_class.new(
          attrs,
          options,
          required,
          self,
          opts
        )
        @api.inheritable_setting.add_validation(validator_instance)
      end

      def all_element_blank?(scoped_params)
        scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)

View on GitHub (pinned to 22d7975629)

Solutions

  1. Express each variant as a lambda/proc: `oneof: [-> { requires :name, type: String }, -> { requires :id, type: Integer }]`
  2. Wrap named methods as `-> { by_name_variant }` so the variant is still a Proc
  3. For plain type unions (not structural alternatives), use `types: [Integer, String]`, not `oneof:`

Example fix

# before
requires :filter, type: Hash, oneof: [:by_name, :by_id]

# after
requires :filter, type: Hash, oneof: [
  -> { requires :name, type: String },
  -> { requires :id, type: Integer }
]
Defensive patterns

Strategy: type-guard

Validate before calling

def oneof_variants_valid?(opts)
  variants = opts[:oneof]
  opts[:type] == Hash && variants.is_a?(Array) && !variants.empty? && variants.all?(Proc)
end

Type guard

def oneof_shape?(opts)
  opts[:type] == Hash && opts[:oneof].is_a?(Array) && opts[:oneof].any? && opts[:oneof].all?(Proc)
end

return unless oneof_shape?(filter_opts)

Try / catch

begin
  requires :filter, type: Hash, oneof: variant_list
rescue ArgumentError => e
  raise "oneof variants must be procs (got #{variant_list.map(&:class).inspect}): #{e.message}"
end

Prevention

When it happens

Trigger: `oneof: [:by_name, :by_id]` (symbols); `oneof: ['name', 'id']` (strings); `oneof: [-> { requires :id }, { type: String }]` (mixed proc and hash).

Common situations: Expecting `oneof:` to accept a plain list of type names the way `types:` does; refactoring named methods into variants without converting them to lambdas.

Related errors


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