Skip to content

Document the definition-time rejection of uncoercible collection types - #2837

Open
ericproulx wants to merge 1 commit into
masterfrom
upgrading-array-unknown-element-type
Open

Document the definition-time rejection of uncoercible collection types#2837
ericproulx wants to merge 1 commit into
masterfrom
upgrading-array-unknown-element-type

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

#2817 has an unrecorded breaking side effect. Declaring type: Array[Foo] / type: Set[Foo] where Grape cannot coerce Foo now raises while the API class is being loaded:

ArgumentError: type Foo should support coercion via `[]`

An application that boots today can fail to boot on 4.0, without any request being made. This adds the UPGRADING entry and pins the behavior with specs.

Found while investigating grape-swagger's suite failing against grape master — 12 of its failures are this, from fixtures declaring type: Array[Entities::ApiError] on an OpenStruct subclass used purely for documentation.

What actually changed

Nothing about which types are supported — only when the element coercer is built. #2817 made ArrayCoercer build it eagerly in the constructor instead of memoizing it on first use, so that coercers (shared across requests, and cached in CoercerCache) create no state at request time. Building it eagerly means an unsupported element type is discovered at definition.

Before, it was built on the first request that supplied the parameter. So a declaration never exercised by a request never raised at all; one that was exercised got the same ArgumentError swallowed by the coercion validator into a generic 400 {"error":"foo is invalid"}.

This is a correct tightening

declaration 3.3.4 master
type: Foo ArgumentError when defined unchanged
type: Array[Foo] accepted; 400 "is invalid" per request ArgumentError when defined

The non-collection form has always raised at definition time. The collection form's leniency was an accident of the lazy build, not a supported behavior — and it wasn't a working feature but a landmine, since the parameter could never actually be coerced. The two are now consistent. I'm documenting it rather than reverting it.

Verified across 3.2.0 and 3.3.4 (both accept) and bisected on master to e209dbe7 (#2817) as the first commit that rejects.

The UPGRADING entry

Covers what raises, why, the before/after table, and the three fixes that actually work — each verified end-to-end, not just at load:

  • a one-argument self.parse (routes to CustomTypeCollectionCoercer, 200)
  • coerce_with: supplying the whole collection (no element coercer is built, 200)
  • dropping type: for documentation: { type: Foo } when the class only describes the parameter — the case behind this break

It also warns that defining self.[] silences the load-time error without fixing anything: that's the escape hatch for dry-types objects, and a plain class taking it still fails 400 "is invalid" on every request. I nearly listed coerce_with as a non-fix for the same reason before testing it properly — the distinction is easy to get wrong, hence the explicit note.

Specs

Nothing asserted this anywhere — should support coercion via appeared in no spec — so a future refactor could revert it as silently as #2829 reverted #2824. Five examples added under ::build_coercer: Array/Set/params-block rejection, plus the parse and coerce_with acceptances.

The three "raises" examples fail against the pre-#2817 lazy build and pass against the eager one — verified in both directions.

Notes

🤖 Generated with Claude Code

#2817 made ArrayCoercer build its element coercer eagerly in the
constructor rather than memoizing it on first use, so that coercers --
shared across requests -- create no state at request time. A side effect
went unrecorded: an Array/Set whose element type Grape cannot coerce is
now rejected while the params block is evaluated, so an application that
boots today can fail to boot on 4.0 with

    ArgumentError: type Foo should support coercion via `[]`

Previously the element coercer was built on the first request that
supplied the parameter, so a declaration never exercised by a request --
a documentation-only type, which is what this breaks in practice -- never
raised at all; one that was exercised got the same ArgumentError swallowed
by the coercion validator into a generic 400 "is invalid".

The tightening is right: the non-collection form (type: Foo) has always
raised at definition time, and the collection form's leniency was an
accident of the lazy build rather than a supported behavior. But it is a
load-time break, so it needs an UPGRADING entry, which this adds -- with
the three fixes that actually work (a one-argument parse, coerce_with, or
dropping type: in favour of documentation:), and a warning that defining
self.[] only silences the load-time error while leaving the parameter
failing per request.

Also pin the behavior: nothing asserted it anywhere, so a future refactor
could revert it silently. The three "raises" examples fail against the
pre-#2817 lazy build and pass against the eager one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx requested a review from dblock July 27, 2026 22:29
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.

1 participant