Rails Backend: Custom Validations

Sylwia Vargas - Apr 11 '20 - - Dev Community

Ruby on Rails has amazing docs on validations. In this blog post I want to cover custom validations but I will also give an intro to the standard Active Record validations that Rails gives us in its infinite kindness.

Read about:


Active Record validations

There's already so much Active Record is doing for you under the hood. Take for instance this method that checks whether an instance we are trying to create has all the attributes needed:



class Person < ApplicationRecord
  validates :name, presence: true
end

Person.create(name: "Frida Kahlo").valid? # => true
Person.create(name: nil).valid? # => false


Enter fullscreen mode Exit fullscreen mode

You can go fancy and require that name is unique, of particular length, contains (or does not) certain characters, etc. They even have an email validation that uses regex. You can also specify the message that will come if any of the validations is violated. I love the docs on validations so instead of dwelling more on them, I'll just move onto writing custom validators.


Custom validations: methods

Now, what would happen if what you want to validate is not covered by Rails safety umbrella? Say you want all your users to have at least 3 letters "o" in their username.

1. Validate and custom method

When you want to implement custom validations, you'll use validate instead of validates and add the method name that you declare below it. Here's a user class with validations and pseudocoded method — note that I made the custom validation a private method:



class User < ApplicationRecord

validates_presence_of :username
validate :has_three_os

private

 def has_three_os
   # take the username attribute
   # check if it contains three o's
   # if not, show an error message: "your username needs to contain three o's"
 end
end


Enter fullscreen mode Exit fullscreen mode

2. Write the method

So how are we going to tap into user's username if we are not passing in an argument? Well, when validate calls on the has_three_os method, it passes a user object to be created so we can just tap into that by calling self.



def has_three_o
   number = self.username.scan(/o/).length
   if number < 3
    self.errors.add(:username, "needs to have three o's and #{self.username} has only #{number}!")
   end
end


Enter fullscreen mode Exit fullscreen mode

Now if we try to create a user with the username of "Frida Kahlo", we'll get:

ActiveRecord::RecordInvalid (Validation failed: Username needs to have three o's and Frida Kahlo has only 1!)

Sorry, Frida!


Custom validations: classes

Now, imagine that we also have a Monkey model and we want the monkey's name to also have 3 o's. We'd need to copy-paste the same method and that's not very DRY. How about we place it someplace else? What you may find exciting to learn is that you can build your own Rails validator!

1. New folder, new file, new class

For the organizational purposes, create a new folder in your app folder: app\validators. Inside, create a new file, the name of which should describe what this code does. I names mine HasThreeOsValidator.rb. Now, inside write a class that inherits from ActiveModel::EachValidator:



class HasThreeOsValidator < ActiveModel::EachValidator
end


Enter fullscreen mode Exit fullscreen mode

2. Use the validation in the model

Now, I will use this validator in the User model:



class User < ApplicationRecord
    validates :username, has_three_os: true
end 


Enter fullscreen mode Exit fullscreen mode

As you see, you use it just like any other standard Rails validator: first call the validates method, then specifying the attribute that needs to be validated and then calling the validator with the desired value.

3. Inspect validate_each method

Now, let's get back to our validator. In Rails validators have access to validate_each method that accepts three arguments: record, attribute and value. Let's put a byebug inside the method to see what these arguments are:



class HasThreeOsValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
   byebug
  end
end


Enter fullscreen mode Exit fullscreen mode

Now, in our terminal, let's run rails c and in this console, let's create a new User instance: User.create!(username: "Frida", password: "Kahlo2020"):

Now, let's call each argument:



record
# => #<User id: nil, username: "Frida", password: "Kahlo2020", created_at: nil, updated_at: nil>


Enter fullscreen mode Exit fullscreen mode

record tells us about the instance in which the validator was used. This can be helpful to e.g. check if the username is the same as password.



attribute
# => :username


Enter fullscreen mode Exit fullscreen mode

attribute indicates on which attribute this validator is called.



value
# => "Frida"


Enter fullscreen mode Exit fullscreen mode

value tells us the user input for this attribute.

4. Write out your method

Now that we know what each parameter is doing, we can finally write our method. Here we will use the value parameter to check the username:



class HasThreeOsValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
   number = value.scan(/o/).length
  end

end


Enter fullscreen mode Exit fullscreen mode

What happens if the number is less than 3? Such a situation should result in an error. For that, let's use ActiveRecord Error method add. This method accepts three arguments:

  • attribute, which specifies what the scope of the error should be — it can be either the attribute itself, or :base, which would just add an error to the whole record;
  • message, is the message that will be shown for violating this validation; if the message is non-existent, it will just default to "is invalid"; message can either be a symbol or a hash — in majority of cases it's going to be a symbol with :blank key to have the message display nicely (see below)
  • options, which I won't cover here.

Let's add our error and see what happens:



class HasThreeOsValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
   number = value.scan(/o/).length
   if number < 3 
    record.errors.add(attribute, :blank, message: "needs to contain 3 o's") 
   end
  end

end


Enter fullscreen mode Exit fullscreen mode

And this is the error we will get now:

ActiveRecord::RecordInvalid (Validation failed: Username needs to contain 3 o's)

Now our validator can be use across different classes.


Cover picture by Luna Sea. Licensed under CC-BY.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .