[Ruby] [Rails/ActiveRecord] I want to validate the value before type conversion (_before_type_cast)

2 minute read

Introduction

In ActiveRecord, if you enter a character string in a Date type or Integer type column, it will be automatically converted to the column type.

For example, if you have the following users table. (Created_at and updated_at are omitted for simplicity.)

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.integer "age"
  end

Let’s try using the Rails console to enter various values.

irb(main):001:0> User.new(name: "Taro", age: 21) Try entering a number in #age
=> #<User name: "Taro" ,age: 21>

irb(main):002:0> User.new(name: "Taro", age: "21") Try putting a string in #age
=> #<User name: "Taro", age: 21>

irb(main):003:0> User.new(name: "Taro", age: "21") Try putting a character string in #age (1 is a full-width number)
=> #<User name: "Taro", age: 2>

The first is the numerical value. Since it is a numerical value, it can be entered as it is.

The second is a string. The string “21” has been converted to the number 21. That’s excellent.

How about the third one, the 1 part of “21” is a full-width number. How old is age: 2. It is completely different from 21.

By the way, even if you do “2 1” (with a space between them) or “2 a” (with a non-numeric character), it will be age: 2.

What’s wrong

Suppose that you try to play input other than numbers and apply the following validation.

  validates :age, format: {with: /\A[0-9]+\z/}

In this case, if the user inputs “2 1”, “2 a”, etc., the age 2 will be validated and saved as correct data.

Solution “_before_type_cast”

As a solution, if you add _before_type_cast to age, you can validate the value entered by the user instead of the value after the type conversion. *Reference “rails/validates before_type_cast”

  validates :age_before_type_cast, format: {with: /\A[0-9]+\z/ }, presence: true

Application (character string conversion, etc.)

In addition to validation, _before_type_cast is also useful when you want to convert alphanumeric characters in a character string from full-width to half-width, remove white space, and save.

As an example, the following code uses goji called Moji to convert full-width to half-width.

This time, we want to align half-width and full-width input such as user’s “21” (1 is full-width) to half-width before validation, so we use _before_type_cast.

Age_before_type_cast receives the character string as it is input by the user and converts it, then puts the converted character string in age.

  validates :age_before_type_cast, format: {with: /\A[0-9]+\z/ }, presence: true

  before_validation do
    string = self.send(:age_before_type_cast)
    string = Moji.normalize_zen_han(string)
    send(:write_attribute, :age, string)
  end

Properties of _before_type_cast

Difference between 〇〇 and 〇〇_before_type_cast

First is the difference between normal attributes and attributes with _before_type_cast. The value cannot be acquired with [:○○_before_type_cast]. In particular

irb(main):001:0> user = User.new(name: "Taro", age: "21")

irb(main):002:0> user.age
=> 21
irb(main):003:0> user.age_before_typecast
=> "21"

irb(main):004:0> user[:age]
=> 21
irb(main):005:0> user[:age_before_typecast]
=> nil

You can get the value with user.age_before_type_cast, When I try to get the value using user[:age_before_type_cast], nil is returned.

〇〇_before_type_cast cannot be updated

irb(main):001:0> user.age = 21
=> 21
irb(main):002:0> user.age_before_type_cast = 21
=> I get #NoMethodError.

Since _before_type_cast is for checking the input before type conversion to age, it is of course not possible to update itself.

What happens after save

Also, when saved, XX and XX_before_type_cast will be the same. In particular,

irb(main):001:0> user = User.new(name: "Taro", age: "21")
irb(main):002:0> user.save
irb(main):003:0> user.age_before_typecast
=> 21

The number 21 is returned instead of the string “21”.

Because of the above properties, be careful when implementing and testing!

Reference article

[rails/validates before_type_cast](https://dora.bk.tsukuba.ac.jp/~takeuchi/?%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6(%E3%82%A7%E3%82%A2%2Frails%2Fvalidates%E3%81%A7before_type_cast)