Honeybadger Staff
Posted on January 18, 2023
This article was originally written by Aestimo Kirina on the Honeybadger Developer Blog.
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
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](https://github.com/iamaestimo/active-record-validations-tutorial).
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.
```ruby
# 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:
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
rule.
```ruby
# 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).
It's important to point out that the
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).
```ruby
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:
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:
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
option. This means that you now check whether a given input value is an instance of [Numeric](https://ruby-doc.org/core-3.1.1/Numeric.html).
Visit the [Rails Guides](https://guides.rubyonrails.org/active_record_validations.html) 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:
```ruby
# 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.
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
option.
Using our example, let's assume that we want to mark a user as "active" when the user confirms their email address:
```ruby
# 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:
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
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](https://guides.rubyonrails.org/active_record_validations.html#working-with-validation-errors).
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.
```ruby
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:
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!
Posted on January 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024