How to convert to `params.expect` in Rails 8.0

December 21, 2024

After updating RubyGems.org to use the new params.expect feature in Rails 8, I thought it might be helpful to go over a few of the challenges I ran into.

Why Should I Convert to params.expect?

The new expect method for filtering params protects against user param tampering that can cause hard to rescue errors.

As a quick review of the feature, when we have code like this:

user_params = params.require(:user).permit(:name, :handle)

We’re vulnerable to users calling our action like this:

post "/users", params: { user: "error" }
user_params = params.require(:user).permit(:name, :handle)
# undefined method `permit' for an instance of String

By using the new params.expect we can prevent this problem (and another I’ll discuss below) all while cleaning up our params handling.

post "/users", params: { user: "error" }
user_params = params.expect(user: [:name, :handle])
# responds as if the required :user key was not sent at all, rendering a 400 error

Remember that with valid params, the values at the expected key(s) will be returned, just like with require.

post "/users", params: { user: { name: "Martin", handle: "martinemde" }
user_params = params.expect(user: [:name, :handle])
# => { name: "Martin", handle: "martinemde" }

The Easy Part

The conversion follows a consistent pattern.

# OLD
user_params = params.require(:user).permit(:name, :handle)

# NEW
user_params = params.expect(user: [:name, :handle])

For methods that permit a mix of scalars and hashes, the conversion is also straightforward:

# OLD
user_params = params.require(:user).permit(:name, :handle, :image, address: [:street, :city])

# NEW
user_params = params.expect(user: [:name, :handle, :image, { address: [:street, :city] }])

The Challenge

The challenge comes when you have conditional or complex parameter handling, especially when you’re doing something like this:

# OLD: Conditional parameters
user_params = params.require(:user).permit(:name, :handle)
user_params[:admin] = true if current_user.admin?

# OR

# OLD: Multiple permit calls
base_params = params.require(:user).permit(:name, :handle)
admin_params = params.require(:user).permit(:admin) if current_user.admin?
user_params = base_params.merge(admin_params || {})

Solution 1: Extract All Parameters First

The simplest approach is to extract all the parameters you might need and then conditionally use them:

# NEW: Extract all potential parameters
if current_user.admin?
  user_params = params.expect(user: [:name, :handle, :admin])
else
  user_params = params.expect(user: [:name, :handle])
end

Or more concisely:

# NEW: Conditional parameter list
permitted_keys = [:name, :handle]
permitted_keys << :admin if current_user.admin?
user_params = params.expect(user: permitted_keys)

Solution 2: Use Multiple expect Calls

For complex cases, you can still use multiple expect calls:

# NEW: Multiple expect calls
user_params = params.expect(user: [:name, :handle])
user_params[:admin] = params.expect(user: [:admin])[:admin] if current_user.admin?

Though this is less elegant than the single-call approach.

Solution 3: Helper Methods

For really complex parameter handling, consider extracting logic into helper methods:

private

def user_params
  if current_user.admin?
    params.expect(user: [:name, :handle, :admin])
  else
    params.expect(user: [:name, :handle])
  end
end

def user_params_with_conditional_admin
  base_params = params.expect(user: [:name, :handle])
  return base_params unless current_user.admin?

  admin_params = params.expect(user: [:admin])
  base_params.merge(admin_params)
end

Testing Your Conversion

When converting to params.expect, make sure to test edge cases:

# Test with tampered params
test "handles tampered user param as string" do
  post users_path, params: { user: "tampered" }
  assert_response :bad_request
end

test "handles tampered user param as array" do
  post users_path, params: { user: ["tampered"] }
  assert_response :bad_request
end

test "handles missing user param" do
  post users_path, params: {}
  assert_response :bad_request
end

Additional Benefits

Beyond security improvements, params.expect also provides:

  1. Clearer intent - The parameter structure is explicitly declared
  2. Better error messages - More specific feedback about what went wrong
  3. Consistent behavior - No more surprises with edge cases

My Experience Converting RubyGems.org

When I converted RubyGems.org to use params.expect, I found that:

  • Most conversions were straightforward
  • Complex parameter handling became more explicit and easier to understand
  • We caught several edge cases we hadn’t properly handled before
  • The code became more self-documenting

Summary

Converting to params.expect improves security and code clarity. Start with the simple cases, and don’t be afraid to use conditional logic or helper methods for complex parameter handling.

The key is to think about your parameter structure upfront and declare it explicitly rather than building it up incrementally.

© 2025 Martin Emde. All rights reserved.