Rails 8 Strong Parameters: The Double-Bracket Fix for Nested Attributes

Published: (January 11, 2026 at 03:11 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Problem

When upgrading to Rails 8 you may start using params.expect—often prompted by RuboCop’s Rails/StrongParametersExpect—to make strong‑parameter contracts more explicit.
A sharp edge appears with nested attributes submitted as indexed hashes: they can be silently filtered out, causing validations to fail in non‑obvious ways.

Model

class Invoice  {
  "date" => "2026-01-11",
  "due_date" => "2026-01-12",
  "client_id" => "123",
  "line_items_attributes" => {
    "0" => {
      "description" => "Web Development",
      "quantity"    => "10",
      "unit_price"  => "150"
    },
    "1" => {
      "description" => "UI Design",
      "quantity"    => "5",
      "unit_price"  => "200"
    }
  }
}

⚠️ Important: line_items_attributes is not an array; it is a hash keyed by dynamic numeric strings ("0", "1", …). This distinction is the root cause of the bug.

Using require/permit (Works)

def invoice_params
  params.require(:invoice).permit(
    :date,
    :due_date,
    :client_id,
    line_items_attributes: [:description, :quantity, :unit_price, :_destroy]
  )
end

With permit, the nested attributes are assigned correctly and the validation passes.

Using expect (Failure)

def invoice_params
  params.expect(
    invoice: [
      :date,
      :due_date,
      :client_id,
      line_items_attributes: [:description, :quantity, :unit_price, :_destroy]
    ]
  )
end
  • No strong‑parameter error is raised.
  • line_items_attributes is filtered out, so no line items are assigned.
  • validates :line_items, presence: true fails and the invoice is not persisted.

Inspecting the result:

invoice_params[:line_items_attributes]
# => #

The declaration line_items_attributes: [:description, :quantity, :unit_price] tells Rails that a single nested hash with those keys is expected. The incoming data, however, is an indexed hash collection.

The Fix: Double Brackets for Collections

To express “a collection of nested hashes” you must wrap the inner array in another array:

def invoice_params
  params.expect(
    invoice: [
      :date,
      :due_date,
      :client_id,
      line_items_attributes: [[
        :id,
        :description,
        :quantity,
        :unit_price,
        :_destroy
      ]]
    ]
  )
end
  • Inner array → permitted keys for each line item.
  • Outer array → indicates a repeated / collection‑like structure.

This matches both:

  • Indexed hashes ("0" => {...})
  • Arrays of hashes ([{...}, {...}])

Test Before and After

# Before fix – no line items created
expect {
  post invoices_path, params: valid_params
}.to not_change(Invoice, :count)
 .and change(LineItem, :count).by(0)

# After fix – line items created
expect {
  post invoices_path, params: valid_params
}.to change(Invoice, :count).by(1)
 .and change(LineItem, :count).by(2)

Shape vs. Declaration Summary

Shapeexpect declaration
Single nested hashline_items_attributes: [:description]
Indexed hash ("0"=>…)line_items_attributes: [[:description]]
Array of hashesline_items_attributes: [[:description]]

If nested records disappear or validations fail unexpectedly, inspect the shape of the incoming parameters, not just the values.

Guidance for Rails 8 Migration

  • RuboCop often suggests replacing params.require(...).permit(...) with params.expect(...).
  • The Rails/StrongParametersExpect cop does not automatically handle the indexed‑hash format used by Rails nested forms (*_attributes). A naïve rewrite can change behavior.
  • For has_many nested attributes, always use the double‑bracket syntax ([[...]]).
  • When dealing with highly dynamic or complex params, it may be reasonable to keep require/permit or to disable the cop locally.

Takeaways

  • params.expect is stricter than permit and requires an explicit structure.
  • Use [[...]] for collections of nested hashes.
  • Validation failures may be the only symptom of a silently dropped nested attribute.
  • Request specs are essential when changing strong‑parameter handling.

If this saved you time—or cost you some before you found it—you’re not alone.

Back to Blog

Related posts

Read more »

Released Gon v7.0.0

Release v7.0.0 Gon v7.0.0 releasehttps://github.com/gazay/gon/releases/tag/v7.0.0 – this major version bump introduces breaking changes. Breaking change: reque...

ZJIT is now available in Ruby 4.0

Article URL: https://railsatscale.com/2025-12-24-launch-zjit/ Comments URL: https://news.ycombinator.com/item?id=46393906 Points: 21 Comments: 9...