How to Use ActiveRecord Scopes with Ransack Searches: A Complete Guide

To use ActiveRecord scopes with Ransack searches, you must explicitly whitelist them by defining the ransackable_scopes class method in your model to return an array of scope symbols, enabling Context#chain_scope to apply them to the relation before predicate processing.

Effectively using ActiveRecord scopes with Ransack searches in the activerecord-hackery/ransack library requires implementing a whitelist security pattern. By default, Ransack returns an empty array from ransackable_scopes and will not execute any scopes, so you must explicitly expose the specific filtering logic you want accessible through the search interface.

Understanding the Whitelist Mechanism

Ransack prevents arbitrary scope execution through a strict whitelist system defined in lib/ransack/adapters/active_record/base.rb (lines 57‑63). The base implementation returns an empty array:

def ransackable_scopes(auth_object = nil)
  []
end

To expose scopes, override this method in your model and return an array of symbols representing the scope names you want searchable. Ransack will only execute scopes present in this whitelist when processing search parameters.

How Ransack Processes Scopes Internally

When you call Model.ransack(params), the library performs a specific sequence of operations defined in lib/ransack/context.rb to validate and apply scopes:

  1. Whitelist verification: Context#ransackable_scope?(key, klass) (lines 70‑72) checks if the search parameter key exists in the array returned by ransackable_scopes.
  2. Scope chaining: If verified, Context#chain_scope(scope, args) (lines 76‑86) inspects the scope's arity and applies it to the underlying ActiveRecord::Relation.
  3. Argument forwarding: The chain_scope method handles boolean values, single values, or splatted arguments based on the scope's defined parameters.
  4. Predicate processing: After applying scopes, Ransack processes standard search predicates on the modified relation.

Implementing Searchable Scopes in Your Models

Basic Scope Whitelisting

Define your scopes and whitelist them by overriding self.ransackable_scopes in your model:


# app/models/employee.rb

class Employee < ApplicationRecord
  scope :activated, ->(boolean = true) { where(active: boolean) }
  scope :salary_gt, ->(amount) { where('salary > ?', amount) }
  
  def self.hired_since(date)
    where('start_date >= ?', date)
  end

  def self.ransackable_scopes(auth_object = nil)
    %i[activated hired_since salary_gt]
  end
end

Role-Based Access with auth_object

The optional auth_object parameter enables dynamic whitelisting based on the current user. Ransack passes this object from the controller to the model, allowing you to expose different scopes to different user roles:

def self.ransackable_scopes(auth_object = nil)
  if auth_object&.admin?
    %i[activated hired_since salary_gt]
  else
    %i[activated]
  end
end

In your controller, pass the user object as the auth_object:

@q = Employee.ransack(params[:q], auth_object: current_user)

Handling Boolean and Complex Arguments

Ransack automatically sanitizes certain argument types before passing them to scopes:

  • Boolean scopes: Triggered by true-ish values (true, 'true', or ['true'])
  • Array arguments: Must be wrapped in an additional array (e.g., salary_gt: [[100_000]]) to prevent Ransack from flattening them
  • Single values: Passed directly as strings or numbers

When a scope accepts arguments, Ransack's chain_scope method inspects the method arity to determine how to forward the supplied values from the query hash.

Customizing Argument Sanitization

By default, Ransack converts string representations of booleans ('true'/'false') to actual boolean values before passing them to scopes. If you need to receive the raw string values unchanged, implement ransackable_scopes_skip_sanitize_args in your model:

def self.ransackable_scopes_skip_sanitize_args
  [:my_raw_scope]
end

This prevents Ransack's automatic boolean conversion for the specified scopes, ensuring the scope receives the exact parameter value submitted in the form.

Complete Implementation Example

Model Definition


# app/models/article.rb

class Article < ApplicationRecord
  has_many :comments
  
  scope :with_long_comments, -> { joins(:comments).where('comments.body LIKE ?', '%---%') }
  scope :published_since, ->(date) { where('published_at >= ?', date) }
  
  def self.ransackable_scopes(_auth_object = nil)
    [:with_long_comments, :published_since]
  end
  
  def self.ransackable_scopes_skip_sanitize_args
    []
  end
end

Controller Implementation

class ArticlesController < ApplicationController
  def index
    @q = Article.ransack(params[:q], auth_object: current_user)
    @articles = @q.result(distinct: true).page(params[:page])
  end
end

Search Form View

<%= search_form_for @q, url: articles_path, method: :get do |f| %>
  <%= f.check_box :with_long_comments, {}, true, false %> 
  <%= f.label :with_long_comments, "With Long Comments" %>
  
  <%= f.label :published_since, "Published Since" %>
  <%= f.date_field :published_since %>
  
  <%= f.submit "Search" %>
<% end %>

When the form submits with published_since: "2024-01-01", Ransack calls Article.published_since('2024-01-01') after verifying it against the whitelist in ransackable_scopes.

Summary

  • Whitelist required: Scopes must be explicitly returned by ransackable_scopes in lib/ransack/adapters/active_record/base.rb or they will be ignored.
  • Security through auth_object: Pass user context to dynamically control scope availability based on roles or permissions.
  • Argument handling: Boolean scopes activate on true-ish values; array arguments require double-wrapping ([[value]]).
  • Internal processing: Context#chain_scope in lib/ransack/context.rb handles arity inspection and argument forwarding.
  • Sanitization control: Use ransackable_scopes_skip_sanitize_args to bypass automatic boolean conversion when raw values are required.

Frequently Asked Questions

Why isn't my ActiveRecord scope executing with Ransack?

Your scope is likely not included in the ransackable_scopes whitelist. By default, this method returns an empty array, preventing any scope execution for security. Override self.ransackable_scopes in your model to return an array containing your scope name as a symbol, such as %i[my_scope_name].

How do I pass an array argument to a Ransack scope?

When a scope expects an array argument, you must wrap the array in an additional array in your search parameters. For example, if your scope is scope :by_ids, ->(ids) { where(id: ids) }, pass by_ids: [[1, 2, 3]] in the query hash rather than by_ids: [1, 2, 3]. This prevents Ransack from flattening the array during parameter processing.

Can I restrict specific scopes to admin users only?

Yes, use the auth_object parameter passed to ransackable_scopes(auth_object). In your controller, pass the current user as the auth_object (Model.ransack(params, auth_object: current_user)), then implement conditional logic in the method to return different scope arrays based on the user's role or permissions.

How do I prevent Ransack from converting my scope arguments to booleans?

If your scope needs to receive the literal string 'true' or 'false' rather than boolean true/false, define ransackable_scopes_skip_sanitize_args in your model and return an array containing the scope name. This skips Ransack's automatic boolean sanitization for those specific scopes while maintaining it for others.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →