Skip to content

Copy only containers between oneof variants instead of deep_dup - #3046

Merged
ericproulx merged 1 commit into
masterfrom
fix/oneof-copy-containers
Oct 31, 2026
Merged

ericproulx merged 1 commit into
masterfrom
fix/oneof-copy-containers

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

OneofValidator gives each variant its own copy of the value, so a variant that fails after coercing or defaulting something doesn't leak that into the next one. It made the copy with deep_dup, which also copies the leaves, and that breaks uploads:

params do
  requires :value, type: Hash, oneof: [proc { requires :upload, type: File }]
end

Every valid upload gets a 400, value does not match any of the allowed schemas, and has since oneof was added in 3.3.0. The same param in a plain Hash scope works.

Tempfile is a delegator and doesn't inherit from Object, so Object#deep_dup is forwarded to the File it wraps and returns a bare File. Types::File.parsed? then fails its is_a?(Tempfile) check.

Validators write by assigning into Hashes and Arrays, so this PR copies only those and shares the leaves.

Isolation between variants

No spec pinned this: the full suite passed with no copy at all. The new specs show what each kind of copy does when the first variant fails and the second wins:

Request deep_dup (before) dup no copy this PR
Nested code: "007", coerced to Integer by the failed variant "007" "7" "7" "007"
The same inside an Array element "007" "7" "7" "007"
Nested default set by the failed variant nil "red" "red" nil
File upload in a variant 400 201 201 201

One limit: a custom coercer that mutates a String in place, rather than returning a new value, would now be seen by later variants, because leaves are shared.

Dropped require

This was Grape's last call to deep_dup, so active_support/core_ext/object/deep_dup is no longer required by lib/grape.rb. An app outside Rails that calls deep_dup while relying on Grape to have loaded it now needs its own require. That was never documented, so there is no UPGRADING entry. grape-swagger and grape-entity don't use it.

Test plan

  • bundle exec rake passes: RuboCop clean, 3162 examples, 0 failures.
  • oneof_validator_spec.rb passes on gemfiles/rack_2_2.gemfile and gemfiles/rack_3_2.gemfile.
  • Mutation-checked: going back to deep_dup, a shallow dup, no copy, not copying Arrays, and copying Hashes one level only each fail at least one new spec.
  • CI green.

🤖 Generated with Claude Code

OneofValidator gave each variant a deep_dup of the value, so that a variant
that fails after coercing or defaulting something doesn't leak that into the
next one. deep_dup also copies the leaves, which breaks uploads: Tempfile is a
delegator and not an Object, so Object#deep_dup is forwarded to the File it
wraps and returns a bare File. Types::File.parsed? then fails its
is_a?(Tempfile) check, and every valid upload inside a oneof variant has been
rejected with "does not match any of the allowed schemas" since oneof was
added.

Validators write by assigning into Hashes and Arrays, so copying those alone
keeps the variants isolated. This was the last call to deep_dup in Grape, so
its require goes too.

No spec pinned the isolation: the suite passed with no copy at all. Add specs
for a nested Hash coercion, an Array element coercion, a nested default and an
upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix/oneof-copy-containers branch from 64b6a94 to 6833503 Compare October 31, 2026 17:13
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx merged commit eb9fb85 into master Oct 31, 2026
69 checks passed
@ericproulx
ericproulx deleted the fix/oneof-copy-containers branch October 31, 2026 17:17
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