Ruby on Rails: how to get error messages from a child resource displayed?

29,962

Solution 1

Add a validation block in the School model to merge the errors:

class School < ActiveRecord::Base
  has_many :students

  validate do |school|
    school.students.each do |student|
      next if student.valid?
      student.errors.full_messages.each do |msg|
        # you can customize the error message here:
        errors.add_to_base("Student Error: #{msg}")
      end
    end
  end

end

Now @school.errors will contain the correct errors:

format.xml  { render :xml => @school.errors, :status => :unprocessable_entity }

Note:

You don't need a separate method for adding a new student to school, use the following syntax:

school.students.build(:email => email)

Update for Rails 3.0+

errors.add_to_base has been dropped from Rails 3.0 and above and should be replaced with:

errors[:base] << "Student Error: #{msg}"

Solution 2

Update Rails 5.0.1

You can use Active Record Autosave Association

class School < ActiveRecord::Base
    has_many :students, autosave: true
    validates_associated :students
end

class Student < ActiveRecord::Base
    belongs_to :school
    validates_format_of :email,
                  :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i,
                  :message => "You must supply a valid email"
end

@school = School.new
@school.build_student(email: 'xyz')
@school.save
@school.errors.full_messages ==> ['You must supply a valid email']

reference: http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html

Solution 3

This is not a public API yet, but Rails 5 stable seems to have ActiveModel::Errors#copy! to merge errors between two models.

user  = User.new(name: "foo", email: nil)
other = User.new(name: nil, email:"[email protected]")

user.errors.copy!(other.errors)
user.full_messages #=> [ "name is blank", "email is blank" ] 

Again, this is not officially published yet (I accidentally find this one before monkey-patching Errors class), and I'm not sure it will be.

So it's up to you.

Solution 4

I have the same issue. no good answer so far. So I solved it by myself. by replacing association error message with detail error message:

create a concern file models/concerns/association_error_detail_concern.rb:

module AssociationErrorDetailConcern
  extend ActiveSupport::Concern

  included do
    after_validation :replace_association_error_message
  end

  class_methods do
    def association_names
      @association_names ||= self.reflect_on_all_associations.map(&:name)
    end
  end


  def replace_association_error_message
    self.class.association_names.each do |attr|
      next unless errors[attr]
      errors.delete(attr)
      Array.wrap(public_send(attr)).each do |record|
        record.errors.full_messages.each do |message|
          errors.add(attr, message)
        end
      end
    end
  end
end

in your model:

class School < ApplicationRecord
  include AssociationErrorDetailConcern
  has_many :students
  ...
end

then you will get you must supply a valid email error message on students attribute of school record. instead of useless message is invalid

Solution 5

I'm not sure if this is the best (or a correct) answer...i'm still learning, but I found this to work pretty well. I haven't tested it extensively, but it does seem to work with rails4:

validate do |school|
  school.errors.delete(:students)
  school.students.each do |student|
    next if student.valid?
    school.errors.add(:students, student.errors)
  end
end
Share:
29,962

Related videos on Youtube

bignay2000
Author by

bignay2000

Updated on July 25, 2020

Comments

  • bignay2000
    bignay2000 almost 4 years

    I'm having a difficult time understanding how to get Rails to show an explicit error message for a child resource that is failing validation when I render an XML template. Hypothetically, I have the following classes:

    class School < ActiveRecord::Base
        has_many :students
        validates_associated :students
    
        def self.add_student(bad_email)
          s = Student.new(bad_email)
          students << s
        end
    end
    
    class Student < ActiveRecord::Base
        belongs_to :school
        validates_format_of :email,
                      :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i,
                      :message => "You must supply a valid email"
    end
    

    Now, in the controller, let's say we want to build a trivial API to allow us to add a new School with a student in it (again, I said, it's a terrible example, but plays its role for the purpose of the question)

    class SchoolsController < ApplicationController
        def create
          @school = School.new
          @school.add_student(params[:bad_email])
          respond_to do |format|
              if @school.save
              # some code
              else
                format.xml  { render :xml => @school.errors, :status => :unprocessable_entity }
              end
          end
        end
    end
    

    Now the validation is working just fine, things die because the email doesn't match the regex that's set in the validates_format_of method in the Student class. However the output I get is the following:

    <?xml version="1.0" encoding="UTF-8"?>
    <errors>
      <error>Students is invalid</error>
    </errors>
    

    I want the more meaningful error message that I set above with validates_format_of to show up. Meaning, I want it to say:

     <error>You must supply a valid email</error>
    

    What am I doing wrong for that not to show up?

  • bignay2000
    bignay2000 about 14 years
    What's the best way to do this in an XML template? Right now I don't have an XML view, I just let render handle it?
  • bignay2000
    bignay2000 about 14 years
    It gets linked and stored into the database as it should. I'm intentionally failing validation by passing a bad email as the problem originally suggests. If I pass a proper email that passes the regex, then I don't get an error. The point here is to intentionally fail, and get the appropriate message into the view.
  • Fred
    Fred about 14 years
    I understand you are deliberately failing the example. I don't think you understand my point. Why have you made the add_student method a class method? In Ruby, self points to the class object in class methods, so the message add_student, while being sent to the instance object, executes with self pointing to the class object rather than the instance object. Perhaps ActiveRecord manages to do the right thing despite this, but unless I'm missing something, the method should not be a class method. I would be grateful if you could tell me what I'm missing.
  • Fred
    Fred about 14 years
    OK, I understand now. The add_student method cannot be a class method; that was a typo in your example. If you send a class method message to an instance object, you get NoMethodError. Rails doesn't recover from calling a class method on an instance object, at least in this case. The code you gave produces a NoMethodError in SchoolsController#create when I tried it out. Ruby instance objects do not see class methods because class methods are singleton methods for the class object. That's what I didn't understand, and now I do. Learn a thing a day, drive ignorance away!
  • mrudult
    mrudult over 8 years
    How can we make this validate method generalized so that I don't have to define it for every association in my model?
  • KenB
    KenB about 7 years
    This works in rails 4.2 as well. The validates_associated :students is redundant, since autosaved associations validate the children by default, unless validate: false is declared.
  • Arthur Corenzan
    Arthur Corenzan about 7 years
    This validates the associated student but it doesn't discriminate which validations failed when you have multiple validations, it only shows a "Student is not valid."
  • Mauricio Moraes
    Mauricio Moraes about 6 years
    Worked well for me. Tks man! I had a class with ActiveModel::Model included and I wanted to included the errors of another associated object. So validates_associated wouldn't work in my case. But your suggestion worked well. kudos
  • James H
    James H almost 2 years
    There's a public API for this now: stackoverflow.com/a/72832405/320737