Skip to content

Conversation

@zakariaf
Copy link

@zakariaf zakariaf commented May 5, 2025

Motivation / Background

This Pull Request introduces conditional inclusion capabilities to ActiveModel::Serialization. Currently, when using serializable_hash with associations, there isn't a built-in mechanism to conditionally include these associations based on the record's state or other runtime conditions. This often requires developers to implement workarounds, such as creating multiple custom serializers, manually checking conditions before serialization, or implementing complex logic within controllers.

This feature adds :if and :unless options to the :include directive, providing a clean, declarative, and idiomatic Rails way to control association inclusion directly within the serialization options, similar to how these options function in other parts of the framework like validations and callbacks.

Detail

This Pull Request enhances ActiveModel::Serialization#serializable_hash to support :if and :unless options within the :include hash for associations.

These options allow developers to specify conditions that determine whether an association should be included in the serialized output. The conditions can be:

  • A Symbol or String representing an instance method name on the object being serialized.
  • A Proc that will be evaluated in the context of the object being serialized using instance_exec.
  • Any other value that evaluates to a boolean.

Implementation Details:

  1. The serializable_add_includes method is enhanced to evaluate these conditions before processing an association.
  2. Two new private helper methods are introduced:
    • should_include_association?(options): Evaluates both :if and :unless conditions.
    • evaluate_condition(condition): Handles the evaluation of different condition types (Symbol, Proc, etc.).
  3. The conditional options (:if, :unless) are removed from the options hash before it's passed down to nested serializable_hash calls for the included association, ensuring correct behavior with nested includes.

Examples:

# Only include notes if the user's `admin?` method returns true user.serializable_hash(include: { notes: { if: :admin? } }) # Using a Proc to check an attribute user.serializable_hash(include: { notes: { if: -> { active? } } }) # Using :unless option user.serializable_hash(include: { notes: { unless: -> { inactive? } } }) # Combining conditions user.serializable_hash(include: { notes: { if: :admin?, unless: -> { name.blank? } } }) # Works with nested conditional includes and other serialization options user.serializable_hash(include: { friends: { include: { notes: { if: :active?, only: [:title], include: { comments: { if: :public? } } } } } }) 

The implementation follows Rails conventions, using instance_exec for Proc evaluation, and is designed to work seamlessly with existing serialization features like :only, :except, and nested includes.

Additional information

This feature offers several benefits:

  • Authorization & Permissions: Easily include associations only if the current context (e.g., current user) permits viewing them.
  • Varying API Responses: Tailor API response content based on the state of the resource without complex controller logic.
  • Performance & Payload Size: Reduce payload size and potentially improve performance by omitting associations that aren't necessary under certain conditions.
  • Flexibility: Customize serialized output based on dynamic runtime conditions in a declarative way.

The implementation is designed to be backward compatible. Comprehensive test coverage is included, verifying various condition types, combinations, nested scenarios, and edge cases.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Unrelated changes should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.
@rails-bot rails-bot bot added the activemodel label May 5, 2025
@zakariaf zakariaf force-pushed the conditional-includes-in-serialization branch from c311062 to 4313e74 Compare May 5, 2025 00:21
Problem ActiveModel::Serialization lets callers include associated records, but there is no built‑in way to include them only when certain conditions on the record hold. Developers must either hand‑roll the logic or build a custom serializer. Changes * Accept :if and :unless inside the :include hash. * Symbol / String → call that method on the object * Proc → instance_exec(&proc) * Other → truthiness * Return [] for collections whose condition is false to keep the key stable. * Preserve existing options (:only, :except, nested :include, etc.). * Added helper methods `should_include_association?` and `evaluate_condition`. * Added API documentation, CHANGELOG entry, and exhaustive tests. Benefits 1. **Authorization** – disclose only what the caller can see. 2. **Performance** – avoid loading heavy associations unnecessarily. 3. **Flexibility** – tailor JSON output to dynamic business rules. This change is fully backward compatible.
@zakariaf zakariaf force-pushed the conditional-includes-in-serialization branch from 4313e74 to 28fd90d Compare May 5, 2025 00:23
@fatkodima
Copy link
Member

serializable_hash is not meant to replace a fully-featured solutions like active_model_serializers etc. It is basically useful for the simplest cases like get some columns from the list, include associations (optionally providing a list of columns for them too).

@zakariaf
Copy link
Author

zakariaf commented May 5, 2025

@fatkodima Thank you for your feedback. I think you're right that serializable_hash should remain a simpler solution rather than trying to compete with full-featured serialization gems.

I can revise my approach to just add a basic :if option for conditional includes, which would allow for simple use cases like:

user.serializable_hash(include: { notes: { if: :admin? } })

Would this simplified approach align better with the intended scope of serializable_hash, or do you think it's still beyond the scope of what this method should handle? I'm happy to close this PR if you feel conditional includes are outside its scope entirely?

@fatkodima
Copy link
Member

I think it's beyond the scope of what this method does, but let it be open - other people might have other opinions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants