rails: ActiveRecord enum: use validation if exists instead of raising ArgumentError

Regarding ActiveRecord::Enum, assume you had something like

class Project < ActiveRecord::Base
  enum color: [:blue, :green, :red]
  validates :color, inclusion: { in: [:blue, :green, :red] }
end

Currently, if a “bad” value (say, “orange”) is passed in, an ArgumentError is raised.

It would be nice if the validation was preferred (with the ArgumentError as a fallback if there was no validation on that attribute) so that you’d be able to return a nicer error on the object later via the standard means:

object.errors.messages
# => {:color=>["is not included in the list"]} kind of thing

About this issue

  • Original URL
  • State: closed
  • Created 10 years ago
  • Reactions: 92
  • Comments: 31 (10 by maintainers)

Commits related to this issue

Most upvoted comments

In Rails 5.2, you can override assert_valid_value in initializers/active_record_enum.rb as follow:

module ActiveRecord
  module Enum
    class EnumType < Type::Value
      def assert_valid_value(value)
        unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
          nil
        end
      end
    end
  end
end

Instead raise ArgumentError. You will set nil to a invalid value.

So, you can validate the enum as follow:

class User < ApplicationRecord
  enum gender: [:male, :female]
  validates :gender, inclusion: { in: genders.keys, message: :invalid }
end

Or if you want to use the value before casting you can use the attribute self.gender_before_type_cast to validate raw input.

You can put a happy face on the situation by volunteering to work on it 😄. Rails is always open to new contributors!

On Feb 28, 2017, at 00:58, andrew morton notifications@github.com wrote:

Ugg… just ran into this. Sad to see that two years later there’s still no built in validation for enums.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

Hi, we do implement a REST API and have something like this in our controller:

def handle_action
  yield
rescue Unauthorized
  render :nothing => true, :status => 401
rescue CanCan::AccessDenied
  render :nothing => true, :status => 403
rescue ActiveRecord::RecordNotFound
  render :nothing => true, :status => 404
# ...
rescue ActiveRecord::RecordInvalid => e
  render :json => { :error => e.message }, :status => 422
rescue => e
  notify_airbrake(e)
  render :nothing => true, :status => 500
ensure
  ...
end

Our model has an enum attribute called kind, and if we receive an invalid value the ArgumentError being raised, the user receives 500 error code. And I don’t see a good way to handle this at our end. We have to manually validate submitted values in controller, create additional services for that, etc. Doesn’t seem right to me.

It would be helpful if it at least raise a more specific error, like ActiveRecord::EnumValueError or something like that.

Just run into this issue. To me, the behavior of AR enum is surprising. I would not expect this code to raise an ArgumentError

class Order
  enum status: { one: 1, two: 2, three: 3 }
  validates :status, inclusion: { in: Order.statuses.keys }
end

order = Order.new
order.status = 'zero'

In my experience, AR enums are used as user-facing attributes A LOT. Community have several gems that addresses enum issues (e.g. enumerize gem or the enum_attributes_validation mentioned above). But I think rails should address these issues and provide better experience with enums out of the box. Would be glad to help with that issue.

Hi there! I’d like to challenge the reason this issue got closed. I believe the fact that we have 240+ (🙀) gems for the enum keyword is one of the effects of closing this conversation.

Would you be so kind re-opening this issue pretty please? 🙏 At least it will suggest we’re open for contributions (and it seems like we are):

You can put a happy face on the situation by volunteering to work on it 😄. Rails is always open to new contributors!

Agree with @chancancode. The current focus of AR enums is to map a set of states (labels) to an integer for performance reasons. Currently assigning a wrong state is considered an application level error and not a user input error. That’s why you get an ArgumentError.

I’m closing this for now but please do as @chancancode proposed and start a discussion on the mailing list.

@tjschuck thank you for using the beta 💛 !

If you are looking for a quick fix you can add this to your application_record.rb. It will suppress the exception when you have a validation for the enum defined.

class ApplicationRecord < ActiveRecord::Base
  def initialize(*)
    super
  rescue ArgumentError
    raise if valid?
  end
end
class Project < ApplicationRecord
  enum color: [:blue, :green, :red]
  validates :color, inclusion: { in: colors }
end

i have made a gem for validating enums inclusion https://github.com/CristiRazvi/enum_attributes_validation

Ugg… just ran into this. Sad to see that two years later there’s still no built in validation for enums.

Folks, please weight on #41730 to get it merged and get this issue closed 🙏 🙌 !!!

I’ve found a solution. Tested by myself in Rails 6.

# app/models/contact.rb
class Contact < ApplicationRecord
  include LiberalEnum

  enum kind: {
    phone: 'phone', skype: 'skype', whatsapp: 'whatsapp'
  }

  liberal_enum :kind

  validates :kind, presence: true, inclusion: { in: kinds.values }
end
# app/models/concerns/liberal_enum.rb
module LiberalEnum
  extend ActiveSupport::Concern

  class_methods do
    def liberal_enum(attribute)
      decorate_attribute_type(attribute, :enum) do |subtype|
        LiberalEnumType.new(attribute, public_send(attribute.to_s.pluralize), subtype)
      end
    end
  end
end
# app/types/liberal_enum.rb
class LiberalEnumType < ActiveRecord::Enum::EnumType
  # suppress <ArgumentError>
  # returns a value to be able to use +inclusion+ validation
  def assert_valid_value(value)
    value
  end
end

Usage:

contact = Contact.new(kind: 'foo')
contact.valid? #=> false
contact.errors.full_messages #=> ["Kind is not included in the list"]

@heaven:

We have to manually validate submitted values in controller

You should absolutely do that 👍

Also we don’t take feature requests on here, try the Rails Core mailing list to see if you can garner support on your proposal. Thanks ❤️

Here’s a variation on @stas’s solution that appears to work in 6.0.3.4:

enum role: { regular: 0, manager: 1, admin: 2 }

validates :role, inclusion: { in: ->(inst) { inst.class.roles.keys } }

# Overwrite the setter to rely on validations instead of [ArgumentError]
def role=(value)
  self[:role] = value
rescue ArgumentError
  self[:role] = nil
end

After I wrote all that stuff in the previous comment, I discovered https://github.com/makandra/assignable_values, which I think is actually a waaaay better solution!

@chancancode Do you think this is still a non issue? Or can I work on providing support for enums through out the entire process? I can work on form builders, scaffolding and validation. Please let me know your thoughts.