初めに
- ruby 2.3.1
- Rails 5.0.2
-
custom validator
やtype cast
の話が出てきますが、この投稿では深くは扱っていません
問題
- Web APIを開発していて、boolean型のカラムのvalidateを行いたく、ググってよくヒットする書き方を試した
- boolean型のアトリビュートに対して型違いの更新リクエストを行いエラーを期待したが、なぜか成功扱い、にもかかわらず値は変わらずで矛盾していた
- model内で
self.アトリビュート名
を使いリクエストした値を取得しようとしたが、なぜかリクエスト以前の値が取得できた(=リクエストが正常に行われていないように見えた)
migrate
db/migrate/20170703183050_create_my_models.rb
class CreateMyModels < ActiveRecord::Migration[5.0]
def change
create_table :my_models do |t|
t.boolean :my_attribute, default: false, null: false
end
end
end
model
app/model/my_model.rb
class MyModel < ApplicationRecord
validates :my_attribute, inclusion: { in: [true, false]}
end
リクエストとレスポンス
PATCHリクエストボディ
{
"my_attribute": 1
}
PATCHレスポンスボディ
{
"my_attribute": false,
"message": "更新が正常に完了しました。"
}
原因
- modelにリクエストが到達すると
type cast
が行われる - 型違いの場合、リクエストされた値は破棄されエラーも出ない
- リクエストされた値が破棄された後の
self.アトリビュート名
の中にはリクエスト以前の値(=正常な値)が入っていた為、validates
に合格していたっぽい 注釈1
解決策
-
before_type_cast
を使う -
アトリビュート名_before_type_cast
と書くことでメソッドが呼び出せ、type cast
される前の値を取得することが出来る - ただ、これだけだと後述の余談の部分に書くような
message
になってしまうためcustom validator
と組み合わせた
model
app/model/my_model.rb
class MyModel < ApplicationRecord
validates :my_attribute, boolean_before_type_cast: true
end
-
validates :アトリビュート名, バリデーター名: true
の形で呼ぶ
validator
app/validator/boolean_before_type_cast_validator.rb
class BooleanBeforeTypeCastValidator < ActiveModel::EachValidator
def validate_each(record, attr, value)
value = record.send("#{attr}_before_type_cast")
if !value.is_a?(FalseClass) && !value.is_a?(TrueClass) && !(value == 'false') && !(value == 'true')
record.errors.add(:base, "#{attr.to_s.humanize}に設定出来ない値が指定されています")
end
end
end
- ファイル名とクラス名の関係性はrailsの規約に適合させる
- 引数の
record
にMyModelオブジェクトが入っている - 引数の
attr
にmodelのvalidates
で指定したmy_attribute
アトリビュートが入っている - 引数の
value
はtype cast
後の値が入っている為使わない - migrateにて
default: false
を指定している為、my_attribute
の値指定が無い場合(例えばcreateリクエストの場合)は、string型の"false"が自動的に補完されてリクエストされ、dbにはboolean型で保存される -
my_attribute
にboolean型の値が指定されたリクエストの場合でも、railsの仕様上string型の"true"または"false"がリクエストされ、dbにはboolean型で保存される。rails consoleで行ってもweb API経由で行っても同じ結果になる - rspecとfactorygirlを使っている場合は注意が必要(後述)
リクエストとレスポンス
PATCHリクエストボディ
{
"my_attribute": 1
}
PATCHレスポンスボディ
{
"message": "更新に失敗しました。My attributeに設定出来ない値が指定されています"
}
rspecとfactorygirlを使っている場合の注意
- factorygirlのfactoryメソッドにて
my_attribute
の値をboolean型で記述しているとboolean型でリクエストが行われる - テストを成功させる為にはvalidatorの条件に
!value.is_a?(FalseClass) && !value.is_a?(TrueClass)
を入れるか、factoryにてstring型で値を指定する - factorygirl以外でboolean型でリクエストが行われるケースがあるか不明な為、validatorの条件に
!value.is_a?(FalseClass) && !value.is_a?(TrueClass)
を追加しておく方が安全且つfactoryの記述時に型を意識しなくても済む
余談 - custom validatorを使わないとこんな感じのmessage
modelをこう書く
app/model/my_model.rb
class MyModel < ApplicationRecord
validates :my_attribute_before_type_cast, inclusion: { in: [true, false]}
end
PATCHリクエストボディ
{
"my_attribute": 1
}
PATCHレスポンスボディ
{
"message": "更新に失敗しました。My attribute before type castに設定出来ない値が指定されています"
}
-
validates :アトリビュート名_before_type_cast, inclusion: { in: [true, false] }
の形で呼ぶ -
アトリビュート名に
_before_type_cast
がくっついてしまう
参考にしたwebページ
武内修@筑波大 - rails/validatesでbefore_type_cast
- 上記のページが大変参考になりました
- というか
rails before_type_cast
でググっても1500件くらいしかヒットしない、ヤバい
その他
- boolean型を検証してちゃんとエラーで返す方法はググっても殆ど情報が無かった。他の人はどうやっているんだろうか
- もっと別のやり方、綺麗な書き方、間違っている点など、ご指摘あれば是非お願い致します
- factorygirlにはハマりました。
追記
rails 6.0.3.2を使ってかなりプレーンに近い条件で再度試したところ、相変わらず型違いが検証されず通ってしまう結果となりました。
これは環境によるんでしょうか。なぞいです
バリデーション(is_adminカラムに注目ください)
リクエストとレスポンス
-
ここはよくわかっていないです。実際にはmigrateで
boolean: true
と書いた部分でvalidateが行われているだけで、validates :my_attribute, inclusion: { in: [true, false]}
は無意味な記述になっていたんじゃないかと推測しています ↩