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

Introduction

In ActiveRecord, if you put a character string in a Date type or Integer type column, it will be automatically converted to that 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 entering various values using the Rails console.

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

 irb (main): 002: 0> User.new (name: "Taro", age: "21") Try putting a character 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 a 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. Wonderful.

How about the third one, the 1 part of "21" is a full-width number. It has become age: 2. It's completely different from 21.

By the way, even if you use "2 1" (with a space between them) or "2 a" (with a character other than a number), it will be age: 2.

What's wrong

Suppose you try to play a non-numeric input 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., validation will be applied to 2 of age, and it will be saved as correct data.

Solution "\ _before_type_cast"

The solution is to add _before_type_cast to age to validate the value entered by the user instead of the value after the type has been converted. [* Reference "before_type_cast with rails / validates"](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)

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

Applications (such as string conversion)

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, or when you want to save after removing spaces.

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

This time, I use _before_type_cast because I want to align the input that is a mixture of full-width and half-width such as "21" (1 is full-width) of the user to half-width before validation.

After receiving the character string as it is input by the user with age_before_type_cast and converting it, the converted character string is put 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

The nature 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 obtained 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
 => #NoMethodError occurs.

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

What happens after save

Also, if you save, 〇〇 and 〇〇_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".

Be careful when implementing or testing because of the above properties!

Reference article

[before_type_cast with rails / validates](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)

Recommended Posts

[Rails / ActiveRecord] I want to validate the value before the type is converted (_before_type_cast)
I want to get the value in Ruby
[Rails] I want to reset everything because the data in the local environment is strange! What to do before that
I want to create a form to select the [Rails] category
I tried to understand how the rails method "link_to" is defined
I want to change the value of Attribute in Selenium of Ruby
[Ruby] I want to extract only the value of the hash and only the key
I want to introduce the committee with Rails without getting too dirty
I want to var_dump the contents of the intent
The code I used to connect Rails 3 to PostgreSQL 10
I want to play with Firestore from Rails
I want to truncate after the decimal point
[Rails] I want to load CSS with webpacker
[Rails] I want to display the link destination of link_to in a separate tab
I want to hit the API with Rails on multiple docker-composes set up locally
I want to judge the range using the monthly degree
I want to know the answer of the rock-paper-scissors app
[Rails] I don't know how to use the model ...
I want to display the name of the poster of the comment
I want to dark mode with the SWT app
I want to authenticate users to Rails with Devise + OmniAuth
I want to define a function in Rails Console
I want to call the main method using reflection
[Rough commentary] I want to marry the pluck method
I want to return the scroll position of UITableView!
I want to simplify the log output on Android
I want to create a generic annotation for a type
I want to add a delete function to the comment function
[Rails] Validate the start time (datetime type) and end time
How to solve the problem when the value is not sent when the form is disabled in rails and sent
I want to return a type different from the input element with Java8 StreamAPI reduce ()
I want to find out if the specified character string is supported by the target character code
I want to bring Tomcat to the server and start the application
I want to make a specific model of ActiveRecord ReadOnly
I want to call a method and count the number
I want to use the Java 8 DateTime API slowly (now)
I want to use the sanitize method other than View.
I want to put the JDK on my Mac PC
I want to give a class name to the select attribute
I want to recursively search the class list under the package
I want to transition to the same screen in the saved state
I want to return multiple return values for the input argument
[Ruby] I want to reverse the order of the hash table
I want to pass the startup command to postgres with docker-compose.
I want to simplify the conditional if-else statement in Java
[Spring Boot] I want to add my own property file and get the value with env.getProperty ().
I want to make the frame of the text box red when there is an input error