16

I'm trying to ensure that a field of my model is a boolean, but my tests keep on failing.

After reading this: Validating boolean value in Rspec and Rails and this Rails: how do I validate that something is a boolean? I ended up doing it like so:

class Model < ActiveRecord::Base

  validates :my_field, :inclusion => { :in => [true, false] }

end

I've tried testing this a few different ways (using rspec and shoulda matchers) and since my tests keep on failing, I'm right now down to the dumbest possible (?) way. Still, the tests don't pass and I'm guessing that there's some mechanism that converts the value somewhere.

Here's what I'm using to find out what's going on:

# create instance without setting value ...

# these work as expected
model_instance.valid?.should be_false      # passes
model_instance.my_field = true
model_instance.valid?.should be_true       # passes
model_instance.my_field = false       
model_instance.valid?.should be_true       # passes

# works as expected
model_instance.my_field = ""
model_instance.valid?.should be_false      # passes

# these should pass but fail
model_instance.my_field = "foo"
model_instance.my_field.should == "foo"    # fails as well, my_field == false
model_instance.valid?.should be_false      # fails

model_instance.my_field = "false"
model_instance.my_field.should == "false"  # fails as well, my_field == false
model_instance.valid?.should be_false      # fails

model_instance.my_field = "123"
model_instance.valid?.should be_false      # fails

model_instance.my_field = "true"
model_instance.my_field.should == "true"   # fails as well, my_field == true
model_instance.valid?.should be_false      # fails

What am I missing? Seems the value is converted in a somewhat logical fashion but where and how to prevent it? How to do this kind of validation properly?

Community
  • 1
  • 1
polarblau
  • 17,649
  • 7
  • 63
  • 84

2 Answers2

40

I don't know where this idea that you need to validate a boolean field as true/false came from, but i've seen it in a few unrelated projects recently so I'm starting to wonder if this isn't a meme that started somewhere.

Its a boolean field, it HAS to be true or false. AR takes care of the implementation details of boolean values for you. If you want a boolean field, create a boolean field in the database. End of story.

Looking at the source, here's the code that converts a value to a boolean:

# File activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb, line 147
147:         def value_to_boolean(value)
148:           if value.is_a?(String) && value.blank?
149:             nil
150:           else
151:             TRUE_VALUES.include?(value)
152:           end
153:         end

And here are the values that equate to their boolean counterparts:

TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set

So taking another look at your code above, everything is as it should be, except that what you're expecting is incorrect. assigning "foo" to a boolean value we see that "foo" is not in the list of acceptable true values, so the boolean field defaults to false, which is what it will return from its accessor later. The boolean value for the field is still :in => [true, false] so the model is valid. "false" and "123" fail for the same reason. "true" is an acceptable value of the boolean true, so the field gets set to true and is still valid for the reason outlined above.

Keith Gaddis
  • 4,113
  • 23
  • 20
  • 1
    Thanks, I didn't know about the `FALSE_VALUES` / `TRUE_VALUES` constants. And your comprehensive answer explains it nicely! Learned something today. – polarblau Mar 02 '11 at 17:30
  • 8
    I think the meme is that when a boolean field has validates_presence_of, it fails validation whenever it's false. The workaround is to use validates_inclusion_in. I'm seeing this behavior myself in Rails 3.0.9. See here: http://www.ruby-forum.com/topic/99285 – Paul A Jungwirth Aug 12 '11 at 12:52
  • 1
    I was reading again the Rails guides again about Validations, and it explained how to validate for a boolean field. My first instinct was "Why would I validate a boolean, since it will always be true or false and never nil?". Trying to find reasons I bumped into this post. I think the meme started there with newcomers, and kinda stuck. "If they explain how, it should be done". I think it was explained for completeness sake... just my 2 cents worth... – Theo Scholiadis Apr 12 '12 at 10:38
  • 10
    The idea is to prevent the field from being `nil`. – thomasfedb Mar 04 '13 at 09:24
  • Interesting about Active Record. They don't use the default ruby behavior of 0 is true. Thanks for looking into it! – aaron-coding Dec 27 '14 at 17:13
  • this doesn't seem to be complete to me. If I use update_attributes({something_that_should_be_bool: nil}) then I still get an exception rather than a neat error. – Confused Vorlon Nov 28 '16 at 15:02
  • I'd say the answer is almost 6 years old now, it may not still apply :) – Keith Gaddis Dec 07 '16 at 05:07
  • 1
    `TRUE_VALUES` has been removed with Rails v5. Check out the new code: https://github.com/rails/rails/commit/a502703c3d2151d4d3b421b29fefdac5ad05df61 – Amree Jul 24 '19 at 06:35
  • @Amree this answer predates that change by quite a bit, but thanks for pointing that out! – Keith Gaddis May 28 '20 at 12:50
  • Yeah, I think you might want to validate inclusion in true/false ... I just had this come up strangely today: 1. For a particular model, one row out of thousands somehow wound up with a `nil` value which threw a 500 error. 2. I have no idea where the `nil` came from, but the record was updated a few days ago. – Dan Brown Apr 07 '22 at 04:07
28

I'm no Rails guru but I'll stick my neck out here and say that you absolutely do want to validate boolean values for :inclusion => { :in => [true, false] }--that is, assuming you do not want to allow nil (NULL) values.

Coming from a database programming background, I've learned to keep in mind that a boolean field can have THREE values: true, false, and NULL. When programming in native SQL, NULLs require special handling (is null instead of = null) that causes a lot of extra work.

In testing with Rails 3.2 today, it was no problem to create an unvalidated boolean field with a nil value and save it to my PostgreSQL database, where it was dutifully stored as NULL. To avoid all the problems that would cause, I'm planning on using this approach for booleans in Rails:

  • Define boolean fields with :null => false in migrations.
  • Use :inclusion => { :in => [true, false] } to validate the field in the model. This gives a nice message if the field is uninitialized (nil).

It's not intuitive at first that I can't use a :presence validation, but it turns out :presence validates that the value is not blank, and the value false is blank. In other words, if you validate :presence => true and set the value to false, the validation will fail. See validates_presence_of in the API and @Paul A Jungwirth's comment on @Karmajunkie's answer.

Mark Berry
  • 17,843
  • 4
  • 58
  • 88
  • 1
    I agree completely. I never want an INSERT to be rejected by the database (very ugly message, usually a 500 error) when it could be caught by a Rails validation instead, complete with a user-friendly helpful error message. – sockmonk Dec 13 '12 at 23:01
  • Sorry, but I can't get the syntax to work out for my model.rb file. I've tried variations of validates :property_name, inclusion (true, false) as well as :inclusion => { in => [true,false] }. Is your answer for use in the model.rb file? – flobacca May 07 '14 at 20:20
  • 1
    @flobacca, yes it goes in a model, just like in the example at the top of the original question. If it is not working for you, you may need to create your own question so you can show specific code examples. – Mark Berry May 08 '14 at 03:39