How to remove validation using instance_eval clause in Rails?
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.
Comments
-
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 over 12 yearsI don't want to monkey patch more than I need, so changing anything in the ActiveModel is not an option for me, really.
-
thomasfedb over 12 yearsI 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 over 12 yearsI 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 over 12 yearsif 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 over 12 yearsThanks 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 over 12 yearsI'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 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 over 12 yearsFrom 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 over 12 yearsMonkey 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 over 12 yearsJust 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 over 12 yearsIf 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 over 12 yearsThis 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 over 12 yearsI 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 over 12 yearsYou'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 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 over 12 yearsIt'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 over 12 yearsIt'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 over 12 yearsI 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 over 12 yearsBut did you remove
module ActiveRecord; class Base; end; end
? -
charlysisto over 12 yearsOne 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 over 12 yearsThe 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 almost 12 yearsBTW, 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 almost 12 yearsI'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 over 11 yearsThanks Nicolas, the
_validators
attribute is exactly what I was looking for. -
Nico about 10 yearsDoesn'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 over 9 yearsScroll down to my answer. Works with 4.1.6
-
jmarceli about 8 yearsAs for Rails 4.2.6 this seems to be the working example: gist.github.com/brenes/4503386
-
Oded Niv almost 7 yearsYes! Finally worked... Can also use
Ninja.validators_on
now -
Passer by about 6 yearsThis should be the accepted answer. It's very explicit as to what it's doing.
-
Nanego over 4 yearsUsing Rails 5, I ended up using this one line. In Ninja class, I added: self.validators_on(:name).each { |val| val.attributes.delete(:name) }