How to get validation messages for all nested records

Viewed 322

Given this models:

class Foo < ApplicationRecord
  has_many :bars
  accepts_nested_attributes_for :bars
end

class Bar < ApplicationRecord
  validates :title, presence: true
end

I want to get the validation message for each nested Bar I create, because ...

Foo.create(bars_attributes: [, { title: nil }, { title: "foo" }, { title: nil }])

... results in @errors=[#<ActiveModel::NestedError attribute=bars.title, type=blank, options={}>], but I don't know which records (in this case, records at index 0 and 2) have failed. Is this possible at all?

3 Answers

Break it down. Instead of trying to do everything in one command (which can only give you a success/failure response), try something like this (assumes it's being passed the details of your required Bar records in the params as an Enumerable (i.e. array or hash)).

@foo = foo.create(field_1: foo_params[:field_1], field_2: foo_params[:field_2])
_failed_bar_records = []
foo_params[:bar_data].each do |_bar_data|
  _bar = Bar.new(foo_id: @foo.id, title: _bar_data[:title])
  _failed_bar_records.push(_bar_data[:title]) unless _bar.save
end

Then, you'll have an array _failed_bar_records that contains data about each failed record. You could obviously push more to this array, like the actual error message.

you could dig down to nested record's errors instead of checking the parent record's errors

@problem = Problem.new(problem_params)

@problem.solutions
# [<Solution:0x0000565379c46170 id: nil, description: "a solution", problem_id: nil, ...>,
#  <Solution:0x0000565379cc2248 id: nil, description: "", problem_id: nil, ...>]

@problem.save

@problem.errors
# <ActiveModel::Errors:..
# @base= <Problem:0x0000565379c33890 ...>,
# @errors=[
#  <ActiveModel::NestedError attribute=solutions.description, type=blank, options={}>
# ]>

@problem.solutions.map(&:errors)
# [
#   <ActiveModel::Errors:...
#    @base= <Solution:0x0000565379c46170 ...>,
#    @errors=[]>,
#
#   <ActiveModel::Errors:...
#    @base= <Solution:0x0000565379cc2248 ...>,
#    @errors=[
#     <ActiveModel::Error attribute=description, type=blank, options={}>
#    ]>
# ]

Elaborating on Lam Phan's answer, validation errors for each child are accessible as a property of the child itself.

foo = Foo.create(bars_attributes: [{title: nil},{ title: "foo" }, {title: nil}])
=> #<Foo:0x0000559761e83c50 id: nil, created_at: nil, updated_at: nil>

foo.bars.map(&:errors).map(&:details)
=> [{:title=>[{:error=>:blank}]}, {}, {:title=>[{:error=>:blank}]}]

foo.bars.map(&:errors).map(&:messages)
=> [{:title=>["can't be blank"]}, {}, {:title=>["can't be blank"]}]

On a different, arguably less elegant path, with a custom method for validation, it's possible to cycle on the received nested records, adding an error that registers the index of the failing ones.

class Foo < ApplicationRecord
  has_many :bars
  accepts_nested_attributes_for :bars
  validate :bars_validator

  def bars_validator
    children_valid = bars.map(&:valid?).all?
    bars.each_with_index do |b,i| 
      b.errors.each do |err|
        msg = "#{i}: #{err.attribute} #{err.message}"
        errors.add(:bars, msg, index: i, src: err)
      end
    end
    children_valid
  end
end

The index is then retrieved through errors.details

foo = Foo.create(bars_attributes: [{ title: nil }, { title: "foo" }, { title: nil }])
=> #<Foo:0x00005583f6607a50 id: nil, created_at: nil, updated_at: nil>

foo.errors.details
=>
{:"bars.title"=>[{:error=>:blank}],
 :bars=>
  [{:error=>"0: title can't be blank", :index=>0, :src=>#<ActiveModel::Error attribute=title, type=blank, options={}>},
   {:error=>"2: title can't be blank", :index=>2, :src=>#<ActiveModel::Error attribute=title, type=blank, options={}>}]}
Related