How do i specify and validate an enum in rails?
Asked Answered
S

12

50

I currently have a model Attend that will have a status column, and this status column will only have a few values for it. STATUS_OPTIONS = {:yes, :no, :maybe}

1) I am not sure how i can validate this before a user inserts an Attend? Basically an enum in java but how could i do this in rails?

Solarize answered 16/11, 2011 at 5:2 Comment(2)
mu and mike's answers are good - see also https://mcmap.net/q/355247/-what-is-the-best-way-to-handle-constants-in-ruby-when-using-rails/887124 , which gives some slightly different answers to a similar question.Duncandunce
Yeah, I actually use something more similar to the solution Bob provided: gistRoborant
R
50

Create a globally accessible array of the options you want, then validate the value of your status column:

class Attend < ActiveRecord::Base

  STATUS_OPTIONS = %w(yes no maybe)

  validates :status, :inclusion => {:in => STATUS_OPTIONS}

end

You could then access the possible statuses via Attend::STATUS_OPTIONS

Roborant answered 16/11, 2011 at 5:11 Comment(4)
Pre Rails 4.1 this is fine. In Rails 4.1+ use the built in enums: edgeguides.rubyonrails.org/…Roborant
Using "native" enums in rails 4.1 has the same drawbacks of using really native enums in postgresql. You need to take care of the actual order, you can't easily modify the enum once created etc. Moreover, with "native" rails enums, it's a broken feature, because on one side you write and read them as stings, but on the other side you need to query them as numbers. At least with postgresql's enums, everything out of the database will see strings.Legalize
After struggling for an hour trying to get enum work, this solution is far more interesting, and has no drawbacks - can't say the same for Enum.Liaison
Using enum makes database not so readable, you should consult your model to determine what does this 3 value means in database. So, I consider this one a better answer. After all, you can't have that much reputation for nothing :DBrott
S
102

Now that Rails 4.1 includes enums you can do the following:

class Attend < ActiveRecord::Base
  enum size: [:yes, :no, :maybe]

  validates :size, inclusion: { in: sizes.keys }
end

Which then provides you with a scope (ie: Attend.yes, Attend.no, Attend.maybe), a checker method to see if certain status is set (ie: #yes?, #no?, #maybe?), along with attribute setter methods (ie: #yes!, #no!, #maybe!).

Rails Docs on enums

Siglos answered 14/10, 2014 at 16:28 Comment(2)
From what I'm reading rails current implementation of enums are for internal values and not for exposing to users as the question asked (which I think is a shame). I'm getting this from the following thread from February 14. github.com/rails/rails/issues/13971Rectangular
Just a quick note, you have to define enums before the validation line.Hardhearted
R
50

Create a globally accessible array of the options you want, then validate the value of your status column:

class Attend < ActiveRecord::Base

  STATUS_OPTIONS = %w(yes no maybe)

  validates :status, :inclusion => {:in => STATUS_OPTIONS}

end

You could then access the possible statuses via Attend::STATUS_OPTIONS

Roborant answered 16/11, 2011 at 5:11 Comment(4)
Pre Rails 4.1 this is fine. In Rails 4.1+ use the built in enums: edgeguides.rubyonrails.org/…Roborant
Using "native" enums in rails 4.1 has the same drawbacks of using really native enums in postgresql. You need to take care of the actual order, you can't easily modify the enum once created etc. Moreover, with "native" rails enums, it's a broken feature, because on one side you write and read them as stings, but on the other side you need to query them as numbers. At least with postgresql's enums, everything out of the database will see strings.Legalize
After struggling for an hour trying to get enum work, this solution is far more interesting, and has no drawbacks - can't say the same for Enum.Liaison
Using enum makes database not so readable, you should consult your model to determine what does this 3 value means in database. So, I consider this one a better answer. After all, you can't have that much reputation for nothing :DBrott
R
17

This is how I implement in my Rails 4 project.

class Attend < ActiveRecord::Base
    enum size: [:yes, :no, :maybe]
    validates :size, inclusion: { in: Attend.sizes.keys }
end

Attend.sizes gives you the mapping.

Attend.sizes # {"yes" => 0, "no" => 1, "maybe" => 2}

See more in Rails doc

Ranit answered 10/1, 2015 at 10:54 Comment(2)
Essentially the same comment as this one, but this won't work, because enum attributes will throw a InvalidArgument exception on invalid values, before the validation is called. More on that here: github.com/rails/rails/issues/13971Hamlett
@Hamlett is right I tried this solution and ended up with the ArgumentError regardless.Larondalarosa
E
9

You could use a string column for the status and then the :inclusion option for validates to make sure you only get what you're expecting:

class Attend < ActiveRecord::Base
    validates :size, :inclusion => { :in => %w{yes no maybe} }
    #...
end
Eumenides answered 16/11, 2011 at 5:8 Comment(0)
H
9

Rails 7.1+

After this pull request you don't need some hacks. If you want the enum value to be validated before saving, use the option :validate:

class Conversation < ApplicationRecord
  enum :status, %i[active archived], validate: true
end

conversation = Conversation.new

conversation.status = :unknown
conversation.valid? # => false

conversation.status = nil
conversation.valid? # => false

conversation.status = :active
conversation.valid? # => true

It is also possible to pass additional validation options:

class Conversation < ApplicationRecord
  enum :status, %i[active archived], validate: { allow_nil: true }
end

conversation = Conversation.new

conversation.status = :unknown
conversation.valid? # => false

conversation.status = nil
conversation.valid? # => true

conversation.status = :active
conversation.valid? # => true

If not pass this option (or explicitly pass validate: nil or validate: false), validation doesn't invoke and ArgumentError raises in case of invalid value


Before Rails 7.1

Rails automatically validate this value raising error

def assert_valid_value(value)
  unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
    raise ArgumentError, "'#{value}' is not a valid #{name}"
  end
end

What you can is override this method with empty body

# config/initializers/enum_prevent_argument_error.rb
module ActiveRecord
  module Enum
    class EnumType < Type::Value
      def assert_valid_value(_)
      end
    end
  end
end

After that validation validates :status, inclusion: { in: statuses.keys } will work (without this monkey patch ArgumentError raises). Warning: If you apply this monkey patch, you should double check that all enums have this validation. If there is no validation and no check constraint, invalid values will be saved as is in the database and will be as nil attriubute in the AR model

Hondo answered 1/9, 2023 at 20:3 Comment(0)
A
4

What we have started doing is defining our enum items within an array and then using that array for specifying the enum, validations, and using the values within the application.

STATUS_OPTIONS = [:yes, :no, :maybe]
enum status_option: STATUS_OPTIONS
validates :status_option, inclusion: { in: STATUS_OPTIONS.map(&:to_s) }

This way you can also use STATUS_OPTIONS later, like for creating a drop down lists. If you want to expose your values to the user you can always map like this:

STATUS_OPTIONS.map {|s| s.to_s.titleize }
Adriell answered 25/11, 2014 at 20:42 Comment(1)
Rails 5 will automatically generate the class level method .status_options: api.rubyonrails.org/v5.2.3/classes/ActiveRecord/Enum.htmlTush
Q
3

Building on top of @mechnicov's aswner, in case someone else doesn't know ruby very well:

In Rails 7.1 +, if you are defining enums in hash format:

class Meal < ApplicationRecord
    enum meal_type: {
        breakfast: 0,
        lunch: 1,
        snack: 2,
        dinner: 3
    }
end

you will have to change it slightly to pass the validate: true option, namely, changing the colon (:) position and adding an additional comma (,):

class Meal < ApplicationRecord
    enum :meal_type, {
        breakfast: 0,
        lunch: 1,
        snack: 2,
        dinner: 3
    }, validate: true
end
Quince answered 7/12, 2023 at 23:16 Comment(0)
M
0

For enums in ActiveModels you can use this gem Enumerize

Misdeed answered 25/11, 2014 at 23:5 Comment(0)
N
0

After some looking, I could not find a one-liner in model to help it happen. By now, Rails provides Enums, but not a comprehensive way to validate invalid values.

So, I opted for a composite solution: To add a validation in the controller, before setting the strong_params, and then by checking against the model.

So, in the model, I will create an attribute and a custom validation:

attend.rb

enum :status => { your set of values }
attr_accessor :invalid_status

validate :valid_status
#...
private
    def valid_status
        if self.invalid_status == true
            errors.add(:status, "is not valid")
        end
    end

Also, I will do a check against the parameters for invalid input and send the result (if necessary) to the model, so an error will be added to the object, thus making it invalid

attends_controller.rb

private
    def attend_params
        #modify strong_params to include the additional check
        if params[:attend][:status].in?(Attend.statuses.keys << nil) # to also allow nil input
            # Leave this as it was before the check
            params.require(:attend).permit(....) 
        else
            params[:attend][:invalid_status] = true
            # remove the 'status' attribute to avoid the exception and
            # inject the attribute to the params to force invalid instance
            params.require(:attend).permit(...., :invalid_status)
       end
    end
Nestle answered 2/10, 2017 at 11:10 Comment(0)
E
0

To define dynamic behavior you can use in: :method_name notation:

class Attend < ActiveRecord::Base
  enum status: [:yes, :no, :maybe]
  validates :status, inclusion: {in: :allowed_statuses}

  private

  # restricts status to be changed from :no to :yes
  def allowed_statuses
    min_status = Attend.statuses[status_was]
    Attend.statuses.select { |_, v| v >= min_status }.keys
  end
end
Evangelineevangelism answered 2/10, 2019 at 14:23 Comment(0)
D
0

You can use rescue_from ::ArgumentError.

rescue_from ::ArgumentError do |_exception|
  render json: { message: _exception.message }, status: :bad_request
end
Deferral answered 17/1, 2022 at 6:15 Comment(0)
I
0

Want to place another solution.

#lib/lib_enums.rb
module LibEnums
  extend ActiveSupport::Concern

  included do
        validate do
        self.class::ENUMS.each do |e|
          if instance_variable_get("@not_valid_#{e}")
            errors.add(e.to_sym, "must be #{self.class.send("#{e}s").keys.join(' or ')}")
          end
        end
      end

        self::ENUMS.each do |e| 
          self.define_method("#{e}=") do |value|
            if !self.class.send("#{e}s").keys.include?(value)
              instance_variable_set("@not_valid_#{e}", true)
            else
              super value
            end
          end
        end
    end
end
#app/models/account.rb
require 'lib_enums'
class Account < ApplicationRecord
  ENUMS = %w(state kind meta_mode meta_margin_mode)
  include LibEnums
end
Ifill answered 16/3, 2022 at 1:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.