Skip to content

Reject what a collection with multiple member types cannot coerce - #2955

Merged
ericproulx merged 1 commit into
masterfrom
fix/variant-collection-coercion
Sep 19, 2026
Merged

ericproulx merged 1 commit into
masterfrom
fix/variant-collection-coercion

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

A param declared as type: Array[Integer, String], or with such a collection in its types: (including the README's types: [Integer, String, Array[Integer, String]]), is coerced by VariantCollectionCoercer. That coercer checks neither its input nor its output:

params { requires :ids, type: Array[Integer, String] }
post('/x') { params[:ids].inspect }
body Array[Integer] Array[Integer, String] before after
{"ids":"x"} 400 201, ids = nil (despite requires) 400
{"ids":{"a":1}} 400 201, nil 400
{"ids":[1,{}]} 400 201, [1, #<InvalidValue>] 400
{"ids":[1,"2"]} 400 201, [1, 2] 201, [1, 2]
  • A non-Array returned nil instead of an InvalidValue, so CoerceValidator wrote nil over the param.
  • Members went through the member coercer and the results were returned unchecked, so a member none of the types accepts reached the endpoint as an internal InvalidValue object.

Both now answer 400, like a single-type collection. nil and '' still coerce to nil, as the existing specs pin.

A coerce_with method was also only ever called for an Array. A method that splits a String into the collection never ran, and the param came out nil. It now gets the value whatever it is, the same way MultipleTypeCoercer hands it over for types:.

The same behaviour shows up on 3.3.5 and 4.0.1. Found by fuzzing random params schemas.

Backward compatibility

Both changes turn requests that used to pass into 400s, so UPGRADING has an entry. The unit spec that pinned call('not an array')nil now expects an InvalidValue.

Members that are nil are still accepted ([1, null][1, nil]), because the member coercer itself answers nil for them. Array[Integer] rejects them. Changing that is a separate decision, so I left it alone.

Test plan

  • Unit specs for the coercer and request specs in coerce_validator_spec. All five new examples fail on master and pass here.
  • Full RSpec suite passes locally (2916 examples).
  • spec/integration/hashie not run locally (hashie isn't installed). It only covers valid input, which takes the same path as before, so it's left to CI.
  • RuboCop clean.
  • CI green.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the fix/variant-collection-coercion branch from 26ad6e4 to ffc5d5a Compare September 18, 2026 09:52
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx force-pushed the fix/variant-collection-coercion branch 2 times, most recently from 6d49684 to f17a6c4 Compare September 18, 2026 10:30
@ericproulx
ericproulx requested a review from dblock September 18, 2026 10:39
@ericproulx
ericproulx force-pushed the fix/variant-collection-coercion branch from f17a6c4 to fb6068f Compare September 18, 2026 12:52

@dblock dblock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood the behavior change correctly, this is a bug? Maybe state so.

@ericproulx
ericproulx force-pushed the fix/variant-collection-coercion branch from fb6068f to ed5b3b6 Compare September 19, 2026 12:01
A param declared as `type: Array[Integer, String]` -- or with such a
collection among its `types:`, as in the README's
`types: [Integer, String, Array[Integer, String]]` -- is coerced by
VariantCollectionCoercer, which checked neither its input nor its output:

- a value that is not an Array returned nil rather than an InvalidValue,
  so CoerceValidator wrote nil over it and `requires :ids, type:
  Array[Integer, String]` let `ids=x` through as nil;
- each member went through the member coercer and the results were
  returned as they came, so a member none of the types accepts reached the
  endpoint as a Grape::Validations::Types::InvalidValue object.

Array[Integer] answers 400 to both, and now so does a collection with
multiple member types. nil and '' still coerce to nil, as specs pin.

The coercion method, when one is given, was never called for anything but
an Array: a `coerce_with` that splits a String into the collection saw
nothing and the param came out nil. It is now handed the value whatever it
is, as MultipleTypeCoercer hands it for `types:`.

UPGRADING covers both, since each turns a request that used to pass into a
400.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix/variant-collection-coercion branch from ed5b3b6 to 9186a50 Compare September 19, 2026 16:22
@ericproulx
ericproulx merged commit b946e52 into master Sep 19, 2026
69 checks passed
@ericproulx
ericproulx deleted the fix/variant-collection-coercion branch September 19, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants