Skip to content

[Fix] Call #nil? on attribute values only when a nil handler is set#546

Open
Sevenfold777 wants to merge 1 commit into
okuramasafumi:mainfrom
Sevenfold777:avoid-nil-check-without-nil-handler
Open

[Fix] Call #nil? on attribute values only when a nil handler is set#546
Sevenfold777 wants to merge 1 commit into
okuramasafumi:mainfrom
Sevenfold777:avoid-nil-check-without-nil-handler

Conversation

@Sevenfold777

@Sevenfold777 Sevenfold777 commented Jul 16, 2026

Copy link
Copy Markdown

What

A one-line operand swap in Alba::Resource::InstanceMethods#fetch_attribute:

# Before
value.nil? && nil_handler ? instance_exec(obj, key, attribute, &nil_handler) : value
# After
nil_handler && value.nil? ? instance_exec(obj, key, attribute, &nil_handler) : value

#nil? is now called on attribute values only when the resource declares a nil handler (on_nil).

Why

fetch_attribute called value.nil? on every attribute value even when no on_nil handler was set (the common case), and the result was discarded because nil_handler was falsy.

For plain objects this is just one redundant method call per attribute. For lazy-loading proxies such as batch-loader, it is worse: they implement #nil? via method_missing with a side effect that forces the load immediately. When serializing a collection, each record's proxy is forced one by one while the hash is being built, which turns "collect all proxies, then load them in one batch" into N+1 queries. We hit this in production after porting collection serializers from ActiveModelSerializers (which only touches values at encoding time) to Alba.

As far as I can tell, this is the only place where Alba calls a method on every attribute value unconditionally (TypedAttribute#display_value_for only runs for typed attributes on the TypeError path, and select/conditional procs are opt-in). So with this change, lazy values pass through hash building untouched end-to-end; the only remaining consideration for lazy-proxy users is resolution at encoding time, which is backend-dependent and outside fetch_attribute's scope (see Behavior notes below).

Behavior

No documented behavior changes — the entire existing test suite passes unchanged.

  • With on_nil set: #nil? is evaluated exactly as before.
  • Without on_nil: #nil? is no longer called. #nil? on a plain Ruby object is a side-effect-free predicate, and its result was discarded here anyway, so there is no observable difference — it just saves one method call per attribute.

The only way to observe a difference is an object whose #nil? itself has side effects — i.e. lazy proxies, which is what this PR is about. Two notes for such users:

  • Proxies now reach the serialized hash unresolved. Whether encoding triggers their lazy load depends on the backend: backends that call serialization methods on values (like the default JSON one) resolve them at encode time in a single batch — the desired behavior — while Oj-based backends dispatch on the value's actual class, so proxies should be resolved before encoding (e.g. with a custom Alba.encoder that walks the hash).
  • With on_error declared but no on_nil, a proxy whose load fails used to raise while fetching the attribute (where on_error could handle it), because #nil? forced the load there. The error now surfaces at encoding time instead.

Both notes describe objects relying on side effects of an internal evaluation order, not documented Alba behavior, so I kept the change as a plain swap. Happy to target a minor release rather than a patch if you prefer.

Tests

Added two tests in test/usecases/nil_handler_test.rb using a value object that records whether #nil? was called:

  • without on_nil#nil? is not called (fails without this change)
  • with on_nil#nil? is still called (guards handler semantics)

rake test (283 runs), rubocop, and rake typecheck (RBS + Steep) all pass. No sig/ changes needed — no signature change.

Summary by CodeRabbit

  • Bug Fixes

    • Serialization no longer calls an attribute value’s nil? check when no nil-handling option is configured.
    • This improves compatibility with lazy-loading and proxy-backed values.
    • Nil checks continue to run when custom nil handling is configured.
  • Documentation

    • Updated the unreleased changelog to document this behavior change.

`fetch_attribute` evaluated `value.nil?` before checking whether a nil
handler exists, so `#nil?` was called on every attribute value even
when `on_nil` was not used.

For plain objects this is harmless, but lazy-loading proxies such as
batch-loader implement `#nil?` via `method_missing` with a side effect:
it forces the load immediately. When serializing a collection, this
forces each record's proxy one by one and defeats batching (N+1
queries).

Swapping the operands short-circuits on `nil_handler` first:

- With `on_nil` set, behavior is unchanged.
- Without it, `#nil?` is no longer called, which preserves lazy
  proxies' batching and skips one method call per attribute.

Note for lazy proxy users: proxies now reach the serialized hash
unresolved, so they must be resolved before or during encoding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 097463b4-870d-4a53-a010-1ef9308a7343

📥 Commits

Reviewing files that changed from the base of the PR and between 35c7e9b and ca2c88e.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • lib/alba/resource.rb
  • test/usecases/nil_handler_test.rb

Walkthrough

Alba now avoids invoking nil? on attribute values when no on_nil handler is configured. Regression tests cover both handler configurations, and the changelog documents the behavior.

Changes

Nil handling behavior

Layer / File(s) Summary
Nil handler evaluation and regression coverage
lib/alba/resource.rb, test/usecases/nil_handler_test.rb, CHANGELOG.md
fetch_attribute checks for an on_nil handler before calling value.nil?; tests verify predicate invocation with and without a handler, and the changelog records the change.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: okuramasafumi

Poem

A rabbit found a proxy slow,
So skipped nil checks when handlers go.
With on_nil, the check still rings,
Tests hop along on careful springs.
Alba now keeps lazy things in flow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main behavior change around calling #nil? only when a nil handler is configured.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.74%. Comparing base (35c7e9b) to head (ca2c88e).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #546   +/-   ##
=======================================
  Coverage   97.74%   97.74%           
=======================================
  Files          14       14           
  Lines         664      664           
  Branches      116      116           
=======================================
  Hits          649      649           
  Misses         15       15           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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