Ruby on Rails: how to get error messages from a child resource displayed?
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
Related videos on Youtube
bignay2000
Updated on July 25, 2020Comments
-
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 about 14 yearsWhat'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 about 14 yearsIt 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 about 14 yearsI 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 about 14 yearsOK, 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 over 8 yearsHow can we make this validate method generalized so that I don't have to define it for every association in my model?
-
KenB about 7 yearsThis works in rails 4.2 as well. The
validates_associated :students
is redundant, since autosaved associations validate the children by default, unlessvalidate: false
is declared. -
Arthur Corenzan about 7 yearsThis 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 about 6 yearsWorked 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 almost 2 yearsThere's a public API for this now: stackoverflow.com/a/72832405/320737