How to remove validation using instance_eval clause in Rails?

15,610

Solution 1

I found a solution, not sure how solid it is, but it works well in my case. @aVenger was actually close with his answer. It's just that the _validators accessor contains only information used for reflection, but not the actual validator callbacks! They are contained in the _validate_callbacks accessor, not to be confused with _validations_callbacks.

Dummy.class_eval do
  _validators.reject!{ |key, _| key == :field }

  _validate_callbacks.reject! do |callback|
    callback.raw_filter.attributes == [:field]
  end
end

This will remove all validators for :field. If you want to be more precise, you can reject the specific validator for _validators which is the same as the raw_filter accessor of validate callbacks.

Solution 2

I think this is the most actual solution at this moment (I'm using rails 4.1.6):

# Common ninja
class Ninja < ActiveRecord::Base
  validates :name, :martial_art, presence: true
end

# Wow! He has no martial skills
Ninja.class_eval do
  _validators[:martial_art]
    .find { |v| v.is_a? ActiveRecord::Validations::PresenceValidator }
    .attributes
    .delete(:martial_art)
end

Solution 3

Easest way to remove all validations:

clear_validators!

Solution 4

As I was trying to do this to remove the phone validation from the spree Address model, below is the code I got to work. I added the type check for callback.raw_filter because I only wanted to remove the presence validator on the phone field. I also had to add it because it would fail when trying to run against one of the other validators specified in the Spree::Address model that did not have an 'attributes' key for callback.raw_filter, thus an exception was thrown.

Spree::Address.class_eval do

   # Remove the requirement on :phone being present.
  _validators.reject!{ |key, _| key == :phone }

  _validate_callbacks.each do |callback|
    callback.raw_filter.attributes.delete :phone if callback.raw_filter.is_a?(ActiveModel::Validations::PresenceValidator)
  end

end

Solution 5

I had a similar problem and was able to get past it using:

class MyModel << Dummy
  # erase the validations defined in the plugin/gem because they interfere with our own
  Dummy.reset_callbacks(:validate)
  ...
end

This is under Rails 3.0. The caveat: It does remove ALL validations, so if there are others you want to keep you could try Dummy.skip_callback(...), but I could not figure out the right incantation of arguments to make that work.

Share:
15,610
mdrozdziel
Author by

mdrozdziel

:D

Updated on June 16, 2022

Comments

  • mdrozdziel
    mdrozdziel almost 2 years

    I would like to enhance existing class using instance_eval. There original definition contains validation, which require presence of certain fields, ie:

    class Dummy < ActiveRecord::Base
      validates :field, :presence => true 
    end
    

    Now I want to change that to optional using instance_eval (or any other method, really):

    Dummy.instance_eval do
      ...
    end
    

    What would be the proper syntax to remove the validation, so the field is optional. I would rather do this directly on the model layer, instead doing weird hacks in controllers or views. The use of instance_eval is not really required, but as far as I know, this is generally the best way to enhance classes in Rails.

    Edit #1

    In general - the original class is part of the gem and I don't want to fork it, nor tie to specific release. The general cause is not really important. Simply editing the original model has far worse consequences than monkey patching.

  • mdrozdziel
    mdrozdziel over 12 years
    I don't want to monkey patch more than I need, so changing anything in the ActiveModel is not an option for me, really.
  • thomasfedb
    thomasfedb over 12 years
    I never said that. But if you want to work out what adding a validation does to the model (and thus have any hope of reversing it) you'll need to start by reading this file.
  • mdrozdziel
    mdrozdziel over 12 years
    I would rather avoid digging up the solution in the Rails code, because there is a great chance of finding very wrong solution. After all, the reason for posting this question is hope for a quick and semi-clean solution. Worst case scenario for me is reimplementing the whole model in my code.
  • thomasfedb
    thomasfedb over 12 years
    if you're looking for something that's "quick" or "clean" don't look here. If you read the source code carefully you should find a solution. But as per the advice in the question comments, just not doing this at all would seem your best bet.
  • mdrozdziel
    mdrozdziel over 12 years
    Thanks for your input, but it is useless. Pretty much every question on StackOverflow can be answerd by "If you read the source code carefully you should find a solution."
  • thomasfedb
    thomasfedb over 12 years
    I'm linking you the source because there's no "solution on a platter" available for this one. Doing what you're trying is going to be messy and likely to blow-up when you upgrade rails. Do as everybody else has suggested and work out a way to just modify the model in the normal way. Alternately, if that really is impossible, open up the source linked above and work it out.
  • mdrozdziel
    mdrozdziel over 12 years
    "Everybody else" is quite interesting synonim for "Wayne Conrad". Problem is far from being trivial and answering "Go browse the source code" is quite arrogant to say the least. Being compatible with future versions of Rails is least of my concerns. btw: Solution for me started here: casperfabricius.com/site/2008/12/06/…
  • thomasfedb
    thomasfedb over 12 years
    From that link: "I solved it by combining two terrible ugly hacks, and as such I won’t say I found a solution that is anywhere near to be elegant."
  • mdrozdziel
    mdrozdziel over 12 years
    Monkey patching is always considered ugly. But what is the alternative in case, as described in the link? Should the guy just fork the whole Gem and maintain it, in order to fix one tiny thing?
  • thomasfedb
    thomasfedb over 12 years
    Just fork a version for yourself. You might sacrifice future updates, but you can always re-fork in the future. A monkey patch is likely to break eventually anyway and you'll probably spend more time on that they dealing with re-forking the gem.
  • mdrozdziel
    mdrozdziel over 12 years
    If there is very litte changes, I would prefer to go with monkey patching and good set of test instead... But it looks like, this paricular issue (canceling validations) is quite hard to do with Rails... I will propably fill out the optional field on the controller level (it fits the case).
  • mdrozdziel
    mdrozdziel over 12 years
    This solution requires the original model to be altered. This could be a way to go, if one would design the classes from scratch. In this particular question however, there is no such conditional validation. What I need is to simply remove one particular validation defined more-less as specified in the question.
  • mdrozdziel
    mdrozdziel over 12 years
    I can't get this to work. It seems, that this will do only for validations specified after the method override. I have no idea if and how can I get this to load before the class from the Gem is loaded...
  • charlysisto
    charlysisto over 12 years
    You're right self.validates should be redefined before it is called. I guess the best place would be in an initializer. Not totally sure about that... Otherwise I did a quick test and the trick worked... Just beware that I forgot the little star in super(*attributes) - corrected.
  • charlysisto
    charlysisto over 12 years
    @acidburn2k did you try out my last suggestion ? would love feedback on this : it's an interesting problem you came up with.
  • mdrozdziel
    mdrozdziel over 12 years
    It's an interesting idea, but it did not work. The reason is, that if you define ActiveRecord before including Rails, the things will start to break as soon as Rails are included. I believe there are places with calls to check if ActiveRecord was already defined and include if it wasn't. In this case it is defined, but it is simply missing all the code inside... In this particular case Delayed::Job is failing to find set_table_name inside ActiveRecord. :(
  • mdrozdziel
    mdrozdziel over 12 years
    It's not so easy. If you define more fields in one validates call, then you have one PresenceValidator with attributes listed in attributes field. You can only add one condition to the validator, so in this case it would be all or nothing. So in order to introduce conditional validation one has to remove the field from the original validator, and define another one. I know I haven't pointed that out in the original question, but actually this is the case in my code. Besides - hacking the validation is not so trivial. There are more then 1 place you have to look into (see aVenger's answer).
  • charlysisto
    charlysisto over 12 years
    I just discovered you can actually do something much easier : require the file in your Gemfile like so just after gem rails : require "lib/dummy" that way active record is loaded but not the gem yet.
  • charlysisto
    charlysisto over 12 years
    But did you remove module ActiveRecord; class Base; end; end ?
  • charlysisto
    charlysisto over 12 years
    One more thing : when using the solution above you should only write the validation method. Then re-open your class in the usual model folder and add all the rest. By then activerecord is loaded and you should not run into the problem you mentionned. Tell me and if it's not the case I'll try it myself
  • Dominik Grabiec
    Dominik Grabiec over 12 years
    The simplest way would be to create a proxy method to act as the conditional in the validator, that would call both the existing conditional and your new conditional. Most likely you'd want to use AND to merge the conditions. Since this is Rails I would have thought the simplest thing to do was to clone the gem's repository, make your required changes to the library, and point bundler to use your local copy. You then have the power to keep it updated to the original version and you've got your own changes.
  • Nicolas Buduroi
    Nicolas Buduroi almost 12 years
    BTW, yes... this is evil, but can be useful for monkey patching a gem that contains validation rules that you find too strict! DON'T DO THIS AT HOME! ;-)
  • Jack Christensen
    Jack Christensen almost 12 years
    I'll leave this answer as record of what didn't work, but... As it turns out, this works in development, but not in production. Apparently load order is different, and the engine model gets loaded regardless.
  • Joseph Ravenwolfe
    Joseph Ravenwolfe over 11 years
    Thanks Nicolas, the _validators attribute is exactly what I was looking for.
  • Nico
    Nico about 10 years
    Doesn't work anymore with rails 4.1 because _validate_callbacks is not an array anymore, but includes Enumerable, so trying out using reject instead of reject! now...
  • 18augst
    18augst over 9 years
    Scroll down to my answer. Works with 4.1.6
  • jmarceli
    jmarceli about 8 years
    As for Rails 4.2.6 this seems to be the working example: gist.github.com/brenes/4503386
  • Oded Niv
    Oded Niv almost 7 years
    Yes! Finally worked... Can also use Ninja.validators_on now
  • Passer by
    Passer by about 6 years
    This should be the accepted answer. It's very explicit as to what it's doing.
  • Nanego
    Nanego over 4 years
    Using Rails 5, I ended up using this one line. In Ninja class, I added: self.validators_on(:name).each { |val| val.attributes.delete(:name) }