Skip to content

Look up an entity by the object's own class before its element's - #2844

Open
ericproulx wants to merge 1 commit into
masterfrom
fix/entity-lookup-own-class-first
Open

Look up an entity by the object's own class before its element's#2844
ericproulx wants to merge 1 commit into
masterfrom
fix/entity-lookup-own-class-first

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

Grape::DSL::Entity#object_class decided which class to look an entity up for by duck-typing, and considered the object's own class only last:

return object.klass if object.respond_to?(:klass)
return object.first.class if object.respond_to?(:first)

object.class

Both tests are answered by plenty of single objects. A Struct is Enumerable, so it responds to #first; so does any model that includes Enumerable; and #klass is hardly exclusive to ActiveRecord::Relation. For all of those, represent Model, with: Entity was silently ignored — Grape looked up the entity for whatever #first returned, usually found nothing, and serialized the raw object.

Given represent Model, with: Entity and present <one instance>:

presented object before after
plain object entity used entity used
Struct instance raw object entity used
model including Enumerable raw object entity used
object exposing #klass raw object entity used
array of models entity used entity used
empty array raw [] raw [] (unchanged)

Approach

Try the object's own class first, falling back to the collection or wrapped class only when that comes up empty. An Array still resolves through its element class — Array itself has no registered entity — and a relation still resolves through #klass.

Deferring the fallback also stops #first from being called at all when the object resolves on its own class, which matters for anything where taking the first element is expensive or has side effects. There is a spec pinning that: a relation whose #first raises still renders.

Backward compatibility

No UPGRADING entry. The change only makes a registered entity apply where it previously did not, and the objects affected are ones whose represent registration was being ignored. The one theoretical shift is an object whose own class has an entity and whose elements have a different one — the object's own now wins, which is the more specific reading.

Longstanding rather than a regression: the duck-typing dates to 2014 (v0.10.0, 28df2d32) and behaves identically in 3.3.4.

Test plan

  • 5 new examples in api_spec.rb under .represent: Struct, Enumerable model, #klass object, array-still-resolves-by-element, and the #first-not-called guard. Verified 3 of them fail without the lib/ change (the other two are invariant guards).
  • Full RSpec suite passes locally (2548 examples, 0 failures).
  • RuboCop clean.
  • CI green.

🤖 Generated with Claude Code

Grape::DSL::Entity#object_class decided which class to look an entity up for
by duck-typing:

    return object.klass if object.respond_to?(:klass)
    return object.first.class if object.respond_to?(:first)
    object.class

Both tests are answered by plenty of single objects, and the object's own
class was consulted only last. A Struct is Enumerable, so it responds to
#first; so does any model that includes Enumerable; and #klass is hardly
exclusive to ActiveRecord::Relation. For all of those, `represent Model,
with: Entity` was silently ignored and the entity for whatever #first
returned was looked up instead -- usually nothing, so the raw object was
serialized.

Try the object's own class first and fall back to the collection or wrapped
class only when that comes up empty. An Array still resolves through its
element class, since Array itself has no entity, and a relation still
resolves through #klass.

Deferring the fallback also stops #first from being called at all when the
object resolves on its own class, which matters for anything where taking
the first element is expensive or has side effects.

Longstanding: the duck-typing dates to 2014 (v0.10.0) and behaves the same
way in 3.3.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix/entity-lookup-own-class-first branch from 794a2da to e70e858 Compare August 1, 2026 11:26
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

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