Rails Backend: Custom Validations
Tags: rails, beginners
Published: 2020-04-11 03:28:02

Ruby on Rails has amazing [docs on validations](https://guides.rubyonrails.org/active_record_validations.html). 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:
- [ActiveRecord validations](#active-record-validations)
- Custom Validations:
  - [as methods](#custom-validations-methods)
  - [as separate class](#custom-validations-classes)

***

## 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:

```ruby
class Person < ApplicationRecord
  validates :name, presence: true
end
 
Person.create(name: "Frida Kahlo").valid? # => true
Person.create(name: nil).valid? # => false
```
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:

```ruby
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
```

### 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`.

```ruby
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
```
Now if we try to create a user with the username of "Frida Kahlo", we'll get:
![](https://dev-to-uploads.s3.amazonaws.com/i/33a7t4upx3umc70slkrt.png)

`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](https://www.fridakahlo.org/self-portrait-with-monkey.jsp) 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](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). 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`:

```ruby
class HasThreeOsValidator < ActiveModel::EachValidator
end
```
### 2. Use the validation in the model
Now, I will use this validator in the User model:

```ruby
class User < ApplicationRecord
    validates :username, has_three_os: true
end 
```
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:

```ruby
class HasThreeOsValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
   byebug
  end
end
```
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")`:
![""](https://dev-to-uploads.s3.amazonaws.com/i/70l1ie8hluywhyeghkto.png)

Now, let's call each argument:
```ruby
record
# => #<User id: nil, username: "Frida", password: "Kahlo2020", created_at: nil, updated_at: nil>
```
**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. 

```ruby
attribute
# => :username
```
**attribute** indicates on which attribute this validator is called.

```ruby
value
# => "Frida"
```
**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:

```ruby
class HasThreeOsValidator < ActiveModel::EachValidator

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

end
```

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`](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-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:
```ruby
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
```
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](https://www.sketchport.com/drawing/6035533666975744/frida). Licensed under CC-BY.