36

I have two model as follows

class User < ActiveRecord::Base
 validates_associated :account
end

class Account < ActiveRecord::Base

  belongs_to :user

  #----------------------------------Validations--Start-------------------------
  validates_length_of :unique_url, :within => 2..30 ,:message => "Should be atleast 3 characters long!"
  validates_uniqueness_of :unique_url ,:message => "Already Taken"
  validates_format_of :unique_url,:with => /^([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])$/ , :message => " Cannot contain special charaters"
  #----------------------------------Validations--End---------------------------
end

Now when I associate an account to a user it says

"Account is invalid"

Instead I want to get the error message directly from that model. so it should say

"Should be atleast 3 characters long!" or "Already Taken" or " Cannot contain special charaters"

is there a way to do this ?

I don't want to give a generic message like :

validates_associated :account , :message=>"one of the three validations failed"
Gaurav Shah
  • 5,223
  • 7
  • 43
  • 71
  • You should be able to access the account validation messages just like you access the user's validation messages--they're just on the account. – Dave Newton Sep 12 '11 at 12:21
  • I don't want to do like that.. that is my idea behind the question – Gaurav Shah Sep 12 '11 at 12:57
  • So copy the error messages from the account to the user. – Dave Newton Sep 12 '11 at 13:08
  • @Gaurav, did you see the solution I posted? That seems to accomplish what you are looking for. There is no way to make the error messages bubble using the built-in "validates_associated" (if you examine the source code you'll see it doesn't have any option for that). But it's easy to make an error-bubbling version of this validtor, as my post shows. – Ben Lee Sep 12 '11 at 16:02
  • @Dave Newton : How exactly to "copy" ? you mean as Jon Hinson stated ? – Gaurav Shah Sep 13 '11 at 03:56
  • @Ben Lee yup saw your answer.. does the job but not the way I would like to do it. I thought there was a simpler direct solution to this. – Gaurav Shah Sep 13 '11 at 03:56
  • 2
    No, there is no copy there. You would copy the messages for the account to the user; errors are stored in a map, and can be arbitrarily manipulated. Basically a manual version of Ben's answer. His answer, by the way, is precisely what you asked for-when you want something that isn't provided by three framework, you have to do it yourself. His method hides it from the app developer, which seems like what you're asking for. – Dave Newton Sep 13 '11 at 09:40

7 Answers7

30

You can write your own custom validator, based on the code for the built-in validator.

Looking up the source code for validates_associated, we see that it uses the "AssociatedValidator". The source code for that is:

module ActiveRecord
  module Validations
    class AssociatedValidator < ActiveModel::EachValidator
      def validate_each(record, attribute, value)
        if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
          record.errors.add(attribute, :invalid, options.merge(:value => value))
        end
      end
    end

    module ClassMethods
      
      def validates_associated(*attr_names)
        validates_with AssociatedValidator, _merge_attributes(attr_names)
      end
    end
  end
end

So you can use this as an example to create a custom validator that bubbles error messages like this (for instance, add this code to an initializer in config/initializers/associated_bubbling_validator.rb):

module ActiveRecord
  module Validations
    class AssociatedBubblingValidator < ActiveModel::EachValidator
      def validate_each(record, attribute, value)
        ((value.kind_of?(Enumerable) || value.kind_of?(ActiveRecord::Relation)) ? value : [value]).each do |v|
          unless v.valid?
            v.errors.full_messages.each do |msg|
              record.errors.add(attribute, msg, options.merge(:value => value))
            end
          end
        end
      end
    end

    module ClassMethods
      def validates_associated_bubbling(*attr_names)
        validates_with AssociatedBubblingValidator, _merge_attributes(attr_names)
      end
    end
  end
end

So you can now validate like so:

class User < ActiveRecord::Base
 validates_associated_bubbling :account
end

Also, be sure to add a validate: false in your has_many association, otherwise, Rails will validate the association by default and you'll end up with two error messages, one given by your new AssociatedBubblingValidator and one generic given by Rails.

kgangadhar
  • 4,886
  • 5
  • 36
  • 54
Ben Lee
  • 52,489
  • 13
  • 125
  • 145
  • 5
    I honestly think this is actually the most elegant and rubyish way to do it. Not sure what you are looking for by "vanilla" -- there isn't a completely straightforward solution to this problem. – Ben Lee Sep 13 '11 at 07:34
  • I finally give up... your answer seems most appropriate.. will come back if I find a better answer.. Not satisfied as of now. – Gaurav Shah Sep 13 '11 at 09:32
  • In my case unless v.valid? in validates_each method line gave error like "undefined method `valid?' for #". But finally i changed that line into unless v.nil? then next line v.errors.full_message into record.errors.full_message then it worked out for me. But strange issue is all error message came twice(but default "is invalid" message disapper from the list). Then i understand one message come from the model like presence => true and another one is from customer validator.so u need to block either 1 – Madhan Ayyasamy Mar 11 '14 at 07:24
  • http://stackoverflow.com/a/31916721/3192470 here is an answer with a fixed code based on this answer. – Evgenia Karunus Feb 05 '16 at 17:48
  • 1
    @MadhanAyyasamy I edited the answer to make it up-to-date, so it doesn't throw any exceptions on has_many associations (it was before). – sandre89 Jun 27 '20 at 22:05
  • @PaulDaviesC answer should be the accepted answer. No need to create a custom validator for this type of thing anymore. – dft May 06 '21 at 18:26
25

May be you can try something as given below

validates_associated :account , :message=> lambda{|class_obj, obj| obj[:value].errors.full_messages.join(",") }

Through obj[:value] you're accessing the validated Account object. So will obj[:value].errors give you errors.

PaulDaviesC
  • 1,161
  • 3
  • 16
  • 31
  • 1
    Nice. In stabby syntax, that's `validates_associated :account, message: ->(_class_obj, obj){ obj[:value].errors.full_messages.join(',') }` – Jay Jul 05 '16 at 19:21
  • 1
    This should be the accepted answer, no need to create a custom validator when it can be done in 1 or two lines of code. – dft May 06 '21 at 18:25
  • Using the `:message` param means the association name is still prepended to the error message. – David Cook Sep 09 '21 at 00:24
4

A vanilla solution would be to have a second error's rendering for the associated object:

<%= render :partial => 'shared/errors', :locals => {:instance => @account.owner} %>
<%= render :partial => 'shared/errors', :locals => {:instance => @account} %>
clyfe
  • 23,695
  • 8
  • 85
  • 109
2

If anyway needs it, the following works on Rails 5.2

#app/validators/with_own_error_messages_validator.rb
class WithOwnErrorMessagesValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    Array(value).each do |v|
      unless v.valid?
        v.errors.full_messages.each do |msg|
          record.errors.add(attribute, msg, options.merge(value: value))
        end
      end
    end
  end
end

#app/models/project.rb
class Project < ApplicationRecord
  validates :attachments, with_own_error_messages: true
  has_many :attachments, as: :attachable, validate: false
end

Note the validate: false on the has_many association. This is necessary since Rails now validates associations by default. Without it, you get the Attachment error message(s) plus a generic Attachments is invalid.

Hope this helps.

Sig
  • 5,476
  • 10
  • 49
  • 89
1

I ran into the same issue... I want to validate the associated objects, and have the valid? method return false, but not add a (superfluous) message like 'Account invalid'. I just want the account validation messages to appear next to the account fields.

The only way I found to accomplish this is to override run_validations!, like below. In this example, the association is has_many :contacts:

def run_validations!
  valid = super
  contacts.each do |contact|
    valid = false unless contact.valid?(validation_context)
  end
  valid
end

Don't use valid &&= contacts.all? ... because that will prevent the validation on the contacts if the base class is already invalid, and will stop iteration if one of the contacts fails validation.

Perhaps it's an idea to generalize this into some Concern module, but I needed it only once.

1

For those still on Rails 2, you can overwrite validates_associated with the following code:

module ActiveRecord::Validations::ClassMethods
  def validates_associated(association, options = {})
    class_eval do
      validates_each(association) do |record, associate_name, value|
        associate = record.send(associate_name)
        if associate && !associate.valid?
          associate.errors.each do |key, value|
            record.errors.add(key, value)
          end
        end
      end
    end
  end
end

Source: http://pivotallabs.com/alias-method-chain-validates-associated-informative-error-message/

steveklbnf
  • 451
  • 4
  • 7
1

To get the error messages for account, you would have to call the errors method on that instance:

@user.account.errors

or

@account = @user.build_account
@account.errors

or in the view:

<%= error_messages_for :account %>

I'm making the assumption that it's a has_one relationship.

Jon Hinson
  • 498
  • 4
  • 6