Open-source project
brainspec/enumerize avatar
brainspec/enumerize

Enumerize: enumerable attributes with I18n across ActiveRecord, Mongoid and Sequel

Enumerated attributes with I18n and ActiveRecord/Mongoid support

1,760 stars201 forksRubyMIT

At a glance

What is it?
Enumerize is a Ruby gem that turns a model attribute into a typed enumeration with predicate methods, translations and query scopes, across ActiveRecord, Mongoid, MongoMapper and Sequel. Its published releases have stayed on the 2.x line while the repository itself keeps moving.
Who is it for?
Enumerize suits a Rails or Mongoid application that has a column holding a small fixed set of states and wants translations, predicates and scopes without writing them by hand, and it is a good fit when the same enumeration is shared across several models through an extendable module.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 96 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • brainspec/enumerize1,760 ★ · -2
Star history
Repository2026-10-162026-11-07
brainspec/enumerize1.8k1.8k

-2 Stars in the last 22 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

A column of strings that behaves like a type

Enumerize exists because a plain string column holding a status is a source of quiet bugs. Nothing stops code from writing "studnet" into a status field, nothing stops a comparison against the wrong symbol, and nothing gives you a place to put the human readable label for each state. Enumerize attaches an enumeration to a model attribute so the set of allowed values is declared once, near the model, and the rest of the application gets helpers derived from it.

The unit of declaration is a class that extends Enumerize and calls enumerize with a symbol attribute name and an in option holding the allowed values:

ruby
class User
  extend Enumerize

  enumerize :role, in: [:user, :admin]
end

The values are plain identifiers, and the documentation is explicit about the consequence: if you want multi-word or otherwise non-identifier values, you should use the I18n feature rather than encoding the label in the identifier itself. That separation of identifier from display text is the design decision the rest of the gem builds on.

The audience is Ruby developers working with ActiveRecord or one of the supported document mappers. The gem is MIT licensed, the supported versions are Ruby 3.1 or newer and Rails 7.0 or newer, and the repository ships separate Gemfiles for Rails 7.2, 8.0, 8.1 and master alongside one for MongoMapper, so a continuous integration matrix is visible in the file listing.

Defaults, custom stored values, and the validation you did not ask for

Two behaviours are worth knowing before the first deployment, because both can surprise you in production.

The first is validation. By default, enumerize adds inclusion validation to the model, so assigning a value outside the declared set raises a validation error rather than writing to the database. The documentation marks this with a warning and offers skip_validations, either as a boolean or as a lambda evaluated per record, which is how you allow the blank state on a new record but not on a saved one:

ruby
class User < ActiveRecord::Base
  extend Enumerize

  enumerize :status, in: [:student, :employed, :retired], default: lambda { |user| StatusIdentifier.status_for_age(user.age).to_sym }

  enumerize :role, in: [:user, :admin], default: :user
end

A default can be a literal symbol or a lambda that receives the record, so the default status can be derived from the age of the user being built. The lambda is evaluated against the model instance, which is why the example reaches for a named constant rather than inlining logic.

The second is the stored representation. By default the column holds the identifier as a string. You can change that by passing a hash as the in option, mapping identifiers to custom values such as integers:

ruby
class User < ActiveRecord::Base
  extend Enumerize

  enumerize :role, in: { user: 1, admin: 2 }
end

The model still speaks in identifiers, so user.role returns the identifier and user.role_value returns the stored value, and User.role.find_value(:admin).value gives the same answer from the class side. This is how you migrate a column to numeric codes without rewriting the calling code.

Both features are opt-in or opt-out at the attribute level, and both are declared in the same enumerize call, which keeps the definition of an attribute in one place.

Translation scopes, defaults scope, and per-model i18n_scope

I18n support is the reason many teams pick this gem over a hand-rolled enum wrapper. The default translation layout nests under the model name and the attribute name:

yaml
en:
  enumerize:
    user:
      status:
        student: "Student"
        employed: "Employed"
        retired: "Retiree"

If the same attribute name appears on several models, a defaults scope avoids repeating the translations for each of them, since the same block above can be written under en.enumerize.defaults.status instead. Where the naming does not fit that convention, the i18n_scope option sets the scope explicitly, and it accepts a string, an array of strings tried in order, or a proc evaluated per value:

ruby
class Person
  extend Enumerize
  extend ActiveModel::Naming

  enumerize :status, in: %w[student employed retired], i18n_scope: "status"
  enumerize :roles, in: %w[user admin], i18n_scope: ["user.roles", "roles"]
  enumerize :color, in: %w[green blue], i18n_scope: proc { |value| "color" }
end

The array form is a fallback chain, so a value can be translated in one scope and left to another if it is missing from the first. The proc form computes the scope from the value itself, which is the escape hatch when the scope structure does not follow the model at all.

One catch: to use the I18n feature with a plain Ruby object rather than a model, you have to extend ActiveModel::Naming as well. The gem leans on ActiveModel for its naming, so a plain class without it will not resolve translations the way a model does.

Helper methods for text, values, and form options

The generated interface is small and predictable. The text of the current value is available in two equivalent forms:

ruby
@user.status_text # or @user.status.text

The list of possible values for an attribute is available from the class or through the enumerized attributes hash:

ruby
User.status.values # or User.enumerized_attributes[:status].values
# => ['student', 'employed', 'retired']

And the translated text values for the same set come from collecting text over the value objects:

ruby
User.status.values.collect(&:text)
# => ['Student', 'Employed', 'Retiree']

That last distinction matters when you are building a select box, because you need the identifier as the submitted value and the translation as the label. The documented form usage is exactly that, using the options helper:

erb
<%= form_for @user do |f| %>
  <%= f.select :status, User.status.options %>
<% end %>

The options helper supports the :only and :except options, so you can present a subset of the enumeration in a form, for instance when some values are only reachable by the system. The README lists SimpleForm and Formtastic as separate integration sections, so both form builders have documented support rather than being an accident of the options helper.

Predicate methods, their prefixes, and the Mongoid name collision

Reading the current value through a predicate is the ergonomics that removes conditionals from views and queries. The value object itself answers questions about the value:

ruby
user.status = :student
user.status.student? #=> true
user.status.retired? #=> false

Turning the enumeration into predicate methods on the model itself is opt-in, through the predicates option, and it comes with a prefix mode and inclusion filters:

ruby
class User
  extend Enumerize

  enumerize :status, in: %w(student employed retired), predicates: { prefix: true }
end

user = User.new
user.status = 'student'
user.status_student? # => true

With the prefix option, the method name is the attribute name plus the value, which avoids collisions when a model has more than one enumerized attribute. The :only and :except options restrict which values generate methods, so you can keep the generated surface to what is actually used.

There is a documented collision to be aware of. If enumerize is used with Mongoid, it is not recommended to use "writer" as a field value, because writer? is already defined by Mongoid. The project links the issue that documents the conflict. This is the kind of detail that turns a smooth two-hour integration into a debugging session, and it is exactly the sort of thing a generic enum helper cannot warn you about in advance.

A related caveat for the Rails case: the project warns that enumerized values are just identifiers, so multi-word values should be handled through the I18n feature rather than by putting spaces in the identifier.

ActiveRecord scopes and sharing one enumeration across models

Query scopes are generated per enumerized attribute when the scope option is on, and the name is configurable when a bare scope name would clash:

ruby
class User < ActiveRecord::Base
  extend Enumerize
  enumerize :role, in: [:user, :admin], scope: true
  enumerize :status, in: { student: 1, employed: 2, retired: 3 }, scope: :having_status
end

User.with_role(:admin)
# SELECT "users".* FROM "users" WHERE "users"."role" IN ('admin')

The generated pair is with_role and without_role, and the second produces a NOT IN query, which is the form you need for exclusion queries. Scopes compose with each other, so a class method chain like having_status(:employed).with_role(:user,:admin) is a normal relation query rather than a special case.

Sharing an enumeration between models is done with a module that extends Enumerize and is then included wherever the attribute is needed:

ruby
module RoleEnumerations
  extend Enumerize

  enumerize :roles, in: %w[user admin]
end

class Buyer
  include RoleEnumerations
end

class Seller
  include RoleEnumerations
end

This is a small feature with a large effect on a multi-model codebase, because it makes the set of roles a single definition rather than a copy in each class. The trade-off is that the include target now inherits the enumeration through a module, so a reader tracing where the attribute comes from has to look one level up.

Database support covers ActiveRecord, Mongoid and MongoMapper in the documented sections, with MongoMapper using a key declaration and Mongoid using a field declaration, and the README also names Sequel.

A gem whose public releases have not moved since 2021

The maintenance picture is the odd part of this project. The repository is not archived and the last push was on 2026-08-04, so the code is still being touched. The published releases tell a different story: v2.5.0 from 2021-12-24, 2.3.1 from 2019-05-02, and 2.2.2 from 2018-03-06. The most recent tagged release is nearly five years old at the time of writing, while the working tree has moved on since.

That combination is common for a small, stable gem that is maintained without cutting a new version for every change, and the file listing supports that reading. There is a CHANGELOG.md, several Gemfiles targeting different Rails versions including one for master, a Rakefile, an enumerize.gemspec, a spec directory and a test directory, plus an .rspec file, so the test and packaging setup is in place and being maintained. The declared support for Ruby 3.1 and Rails 7.0 or newer is only meaningful if the code is being run against current versions, which points to a maintainer working on compatibility rather than publishing releases.

For an adopter, the practical consequence is that you are likely to be running a version from master rather than a tagged gem release, which means the Gemfile pin should point at a commit or a branch you have read. The CHANGELOG.md is the place to check what changed since v2.5.0.

The gem is MIT licensed, with the licence in the repository as MIT-LICENSE. The other thing a reader should confirm is the test story for their own stack: the repository has both an RSpec suite and a test directory with Minitest and Shoulda integrations documented, but the CI matrix visible in the Gemfile list is a Rails matrix, and MongoMapper is covered by a separate Gemfile rather than the same job.

Editorial conclusion

Enumerize suits a Rails or Mongoid application that has a column holding a small fixed set of states and wants translations, predicates and scopes without writing them by hand, and it is a good fit when the same enumeration is shared across several models through an extendable module. It is a poor fit if you need the value stored as a database enum type, since the documented approach stores identifiers in a plain string column, and it is a poor fit for a project that cannot accept a gem depending on a specific I18n and ActiveModel setup. Before adopting, decide whether your database column should hold the identifier or a custom value, check that the inclusion validation added by default does not conflict with your existing validations, and read the CHANGELOG.md because the published releases have not moved beyond the 2.x line since v2.5.0 on 2021-12-24.

Frequently asked questions

How do I install the enumerize gem?

Add gem 'enumerize' to your application's Gemfile and run bundle, or install it directly with gem install enumerize. The supported versions are Ruby 3.1 or newer and Rails 7.0 or newer.

How do I add an enumerated attribute to a Rails model?

Extend Enumerize in the class and call enumerize with the attribute name and the allowed values, for example enumerize :role, in: [:user, :admin]. With ActiveRecord the column is a plain string column holding the identifier.

Does enumerize add validation to the model?

Yes, by default enumerize adds inclusion validation to the model. You can skip it with the skip_validations option, either set to true or given as a lambda that is evaluated per record, which is how you allow the value on new records only.

How do I translate enumerized values with I18n?

Translations live under an enumerize scope in the locale file, keyed by model and attribute, and a defaults scope lets you share them across models. For other layouts, the i18n_scope option accepts a string, an array of scopes, or a proc evaluated per value. Plain Ruby objects also need to extend ActiveModel::Naming.

Can enumerize store integers instead of string identifiers?

Yes, by passing a hash as the in option, for example enumerize :role, in: { user: 1, admin: 2 }. The model still exposes identifiers through user.role, while user.role_value returns the stored value, and find_value returns the same from the class side.

Official sources

  1. brainspec/enumerize on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/brainspec-enumerize.svg)](https://hysenlabs.com/projects/brainspec-enumerize)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/brainspec-enumerize.svg)](https://hysenlabs.com/projects/brainspec-enumerize)

Was this useful?