A Deep Dive into Active Record Validations

Accepting user input is critical to modern Rails applications, but without validations, it can cause problems. In this article, learn how to use ActiveModel validations to ensure the data you process is safe.

Many activities that used to be done offline can now be done online, from booking flights to opening bank accounts, shopping online, and much more. At the heart of it, we shouldn’t forget that data are powering all these online transactions.

Therefore, to ensure that everything runs seamlessly, the data stored in databases must be of the right quality. However, what is an effective way to ensure data integrity, especially when a big chunk of it is user-generated? This is where data validations are useful.

Ruby on Rails is structured on the MVC architecture. The "M", also called the model layer, is responsible for managing how objects are created and stored in a database. In Rails, the model layer is run by Active Record by default, and as such, Active Record is responsible for handling the important task of data validation.

In simple terms, validations are a set of rules that declare whether a model is valid based on various criteria. Every model object contains an errors collection. In valid models, this collection contains no errors and is empty. When you declare validation rules on a certain model that it fails to pass, then its errors collection will contain errors consistent with the rules you've set, and this model will not be valid.

A simple example is checking whether a certain user input, such as an email field, contains any data after a user form is submitted. If the data is missing, an error message will be shown to the user so that he or she can provide it.

In this article, we'll explore the different validations that come packaged with Active Record, beginning with simple ones: validating the presence of something, validating by data type, and validating by regex. We'll also cover more complex topics, such as how to use validations with Active Storage, how to write your own custom rules, including how to customize the error messages being shown to users, how to test validation rules, and more.

Prerequisites

In this tutorial, we'll be using a simple app featuring Users, Plans, and Subscriptions. With this example, we should be able to cover the range of validation possibilities from the simple to the more complex ones. You can find the example app code here.

You'll also need the following:

  • A working Rails 7 installation
  • Bundler installed
  • Intermediate to advanced Ruby on Rails experience

This should be fun, so let's go!

Built-in Simple Validations

Validating the Presence and Absence Of Attributes

This simple validation is for checking the presence or, in some cases, the absence of an attribute. In the example code below, we're checking to see if the title attribute on our Post model is present before it's saved.

# app/models/user.rb
class User < ApplicationRecord
    # validating presence
    validates :name, presence: true
    validates :email, presence: true
end

So, if you try to submit the form for the user model in the example code above, and it's missing either of the two fields, you will get an error and an error message that the field cannot be blank:

Validating Presence Error

What about validating the absence of an attribute? Where would you utilize that, you may ask?

Well, consider the rampant abuse of forms by bots and other automated scripts. One very simple way of filtering bot form submissions is through the use of "honeypot" fields.

Here, you insert hidden form fields you are sure could never be filled by a normal human user since they would obviously not be visible to them. However, when a bot finds such a field, thinking it's one of the required form inputs, it fills it with some data. In this case, a filled-in honeypot field would indicate that our model is invalid.

#app/models/user.rb
class User < ApplicationRecord
    validates :honey_pot_field, presence: false
end

Let's look into another simple validation with an interesting use case.

Validating Whether Two Attributes Match

To check if two fields submitted by a user match, such as a password field and a password confirmation field, you could use the validates_confirmation_of rule.

# app/models/user.rb
class User < ApplicationRecord
    # validating presence
    validates :name, presence: true
    validates :email, presence: true

    # validating confirmation (that 2 fields match)
    validates :email_confirmation, presence: true
    validates :email, confirmation: true
end

This validation rule works by creating a virtual attribute of the confirmation value and then comparing the two attributes to ensure that they match. In they don't, the user gets a confirmation error (we'll get into errors in detail a bit later in the article).

Validates confirmation error

It's important to point out that the validates_confirmation rule only works if the confirmation field is also present (i.e., it isn’t nil), so make sure to use another rule, the validates_presence_of, to ensure that it's present.

Validating Input Format

Now let's go a little deeper and explore how to validate the format of user inputs using regular expressions.

A particularly good case for using validates_format_of is when you need to be sure that a user is entering a valid email address (i.e., an email address that matches the format of a normal email address, not necessarily whether it's real).

Using our example app, let's ensure that our user inputs a properly formatted email address using a regular expression. We do this by matching what the user inputs to an expected formatted string (see descriptive comments in the code example shown).

class User < ApplicationRecord
    # must match a string that looks like an email; i.e., <string 1>@<string 2>.<string 3>
    validates :email, format: {with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/ }
end

If we try to create a user with an email address that doesn't fit to the expected format specified in the regex, such as "smith@example", we’ll get an invalid email error:

Email validation error

Obviously, regular expressions can be more powerful than the example shown here. If the reader really wants to explore the subject, this resource, although a bit dated, should provide a good starting point.

Next, we’ll exploring another simple validation before moving on to advanced validations and error messages.

Validating the Length of an Input

Again, consider the password field of our example app. Let's say we want to ensure that all users enter a password with at least 8 characters and no more than 15 characters:

class User < ApplicationRecord
    # validating password length
    validates :password, length: 8..15
end

If the user supplies a password whose length is below the range specified in the validation rule, an appropriate error is shown:

Validating input length

For additional examples of simple validation, please see the ever dependable Ruby Guides.

Other simple built-in validations include:

  • Validating inclusion, which checks whether a given input includes values from a provided set and works very well with enumerables.
  • Validating comparison, which is used to compare two values.

Before diving into advanced validations, it would be good idea to note that in Rails 7, the numericality validator now has the only_numeric option. This means that you now check whether a given input value is an instance of Numeric.

Visit the Rails Guides for more examples of built-in validations.

Advanced and Custom Validations

Although quite useful, the validations we've looked at so far are rather simple. To do more, we would need to explore the built-in advanced validations and how to write our own custom validation rules, which will allow us to utilize more complex data validations.

To begin, we’ll learn how to validate the uniqueness of a join model.

Validating the Uniqueness of a Join Model

Using our example, we'd like to ensure that a user can only have one subscription at any given time.

The models from our example app are related as follows: User has many Plans through Subscriptions:

# app/models/user.rb
class User < ApplicationRecord
    has_many :subscriptions
    has_many :plans, through: :subscriptions
end
# app/models/plan.rb
class Plan < ApplicationRecord
    has_many :subscriptions
    has_many :users, through: :subscriptions
end

A Subscription belongs to both the Plan and User models:

# app/models/subscription.rb
class Subscription < ApplicationRecord
  belongs_to :plan
  belongs_to :user
end

Therefore, to ensure that a User can only have a single Subscription, we would have to check that the user_id appears only once in the subscriptions join table using a uniqueness validation rule, as follows:

# app/models/subscription.rb
class Subscription < ApplicationRecord
  belongs_to :plan
  belongs_to :user

  # ensure a user can only have a single subscription
  validates_uniqueness_of :user_id, scope: :plan_id, message: "User can only have one subscription!"
end

If you try to create an additional subscription for a user who already has one, you will get an error (customized for our example). We'll explore errors and custom messages a bit later in the article.

Uniqueness Validation Error

Conditional Validation

It's important to note that all validation rules depend on the Active Model callback API. This means that you can easily use conditionals to determine whether the said validation rule can run.

While implementing the conditional rule, you can pass in additional arguments in the form of a Ruby proc, a symbol, or a string.

Using our example, let's say we'd like for a User to confirm their email address and, during the process, provide their phone number. In such a case, it is necessary for the User model to be persisted in the database in more than one state specifically, when a User has confirmed their email and when they have not.

Thus, to check for the presence of the phone number field, we use a conditional validation:

class User < ApplicationRecord
    # validate phone number on condition
    validates :phone_number, presence: true, if :email_confirmed

    # assumes the user has confirmed their email
    def email_confirmed
        !confirmed.blank?
    end
end

Contextual Validation

It is important to note that all the built-in validations happen when you save a record. However, there are instances where you'd like to modify this behavior, such as when running a particular validation on update and so forth.

Such validations are called contextual validations because they run in a particular context. You create one by passing in the :on option.

Using our example, let's assume that we want to mark a user as "active" when the user confirms their email address:

# app/models/user.rb
class User < ApplicationRecord
    validates :active, inclusion: {in: [true false]}, on: :email_confirmation

    def email_confirmation
        # email confirmation logic here..
    end
end

Custom Validators

Using our example, let's say we'd like to ensure that users can only register using a business email address (i.e., we want to exclude any of the more common and standard email addresses).

To do this, we could still use the built-in validation rules and a regular expression, but for the purposes of our tutorial, let's go the custom route.

The first step is to create a custom validator class that inherits from ActiveModel Validator. Within it, you define a validate method in addition to any number of custom methods you'll be using in the validation process:

# app/validators/user_validator.rb
class UserValidator < ActiveModel::Validator 
    def validate(record)
        if is_not_business_email(record.email) == true
            record.errors.add(:email, "This is not a business email!")
        end
    end

    def is_not_business_email(email_address)
        matching_regex = /^[\w.+\-]+@(live|hotmail|outlook|aol|yahoo|rocketmail|gmail)\.com$/
        std_email_match = email_address.match(matching_regex)
        std_email_match.present?
    end
end

Then, we need to tell our User model to use this custom validator:

# app/models/user.rb
class User < ApplicationRecord
    ...
    validates_with UserValidator
end

Now, if the user now tries to register using the email address matches we've excluded, they will get an error message:

Custom Validator Error

An interesting use-case for validation is when we need to write validation rules for handling Active Storage attachments.

Handling Active Storage Validations

When you consider why you would need validations for Active Storage attachments, it is mostly to ensure the following:

  • Only the allowed file types are stored.
  • The attachments are of a particular size (both in terms of height and width, and the file size).

If you need checks that go beyond these, such as checking whether a certain uploaded file is of a particular mime type, then a custom validator would be the best choice.

For the purposes of this tutorial, let's assume that Users can upload their resumes. In doing so, we need to determine whether the document they are uploading is in PDF format. How do we do this?

The quickest way is to add a custom validation within the User model:

# app/models/user.rb
class User < ApplicationRecord
    # Define the Active Storage attachment
    has_one_attached :resume

    validate :is_pdf

    private

    def is_pdf
       if document.attached? && !document.content_type.in?("application/pdf")
        errors.add(:resume, 'Resume should be PDF!') 
      end
    end
end

You should note that this particular validation will only run when the record is saved, not when the file is uploaded.

When you are doing lots of Active Storage validations, consider using the Active Storage Validator gem.

Errors and Messages

The errors[] object is an array that contains strings with all the error messages for a given attribute. It should return empty if there are no errors.

You can read more about it in the Rails Guides.

For the purposes of this article, let's take a look at an example showing how to pass in attributes to the error message of a particular validation rule.

class User < ApplicationRecord
    # passing attributes to the name error message
    validates_presence_of :name, message: Proc.new { | user, data |
    "#{data[:attribute]} is needed for all registrations!" } 
end

When this validation fails, a custom error message is shown to the user:

Custom Error Message

Conclusion

Application-level validations are an important method of ensuring that the data going into the database is of the right kind.

However, they are not the only ones you can use. Depending on your application's needs, you could also implement database constraints and even client-side validations using something like Stimulus JS.

Happy coding!

What to do next:
  1. Sign up for a FREE Honeybadger account
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Get started free
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Aestimo Kirina

    Aestimo is a family guy, Ruby developer, and SaaS enterpreneur. In his free time, he enjoys playing with his kids and spending time outdoors enjoying the sun, running, hiking, or camping.

    More articles by Aestimo Kirina
    “We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.” 
    Michael Smith
    Try Honeybadger Free for 15 Days
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Try Honeybadger Free for 15 Days
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Try Honeybadger Free for 15 Days
    "Wow — Customers are blown away that I email them so quickly after an error."
    Chris Patton
    Try Honeybadger Free for 15 Days