2
0

More than 1 year has passed since last update.

ActiveModelでパラメータのバリデーションをする

Last updated at Posted at 2022-02-16

はじめに

APIだけ開発して、期待していないJSONでリクエストしたら想定外の挙動になったので、その挙動を紹介します。


期待しない型で送ると500エラーになる

次のパラメータを期待したAPIを考えます。

  • name
    • 文字列型
    • 必須
    • 1文字以上、10文字以下
  • mail
    • 文字列型
    • 必須
    • メールアドレスの正規表現
  • age
    • 数値型
    • 必須
    • 0以上の整数

こんな感じのフォームを作ってバリデーションするかと思います。

email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
      record.errors.add(attribute, 'must be an mail address')
    end
  end
end
sample_form.rb
class SampleForm
  include ActiveModel::Model
  attr_accessor :name, :mail, :age

  validates :name, presence: true, length: { maximum: 10 }
  validates :mail, presence: true, email: true
  validates :age: presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end
form = SampleForm.new(name: nil, mail: 'hoge', age: 1.5)
form.valid? # false
form.errors.to_hash # {:name=>"can't be blank", :mail=>"must be an mail address", :age=>"must be an integer"}

よしよし、それぞれちゃんとバリデーションされていますね。
ちょっとイジワルして別の型で実行してみましょう。

form = SampleForm.new(name: 15, mail: 3, age: '15')
form.valid? # TypeError (no implicit conversion of Integer into String)

不正なパラメータなのでform.valid?がfalseになって400エラーを返したいところでしたが、バリデーション時にエラーになってしまいました。APIのレスポンスは500エラーになることでしょう。

これはEmailValidatorでvalueの型を確かめずに比較をしていたのが原因のようです。

カスタムバリデータでは型チェックを最初にした方がよさそうです。

email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
-   unless /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
+   unless value.is_a?(String) && /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
      record.errors.add(attribute, 'must be an mail address')
    end
  end
end

改めてバリデーションを実行してみましょう。

form = SampleForm.new(name: 15, mail: 3, age: '15')
form.valid? # false
form.errors.to_hash # {:mail=>"must be an mail address"}

メールもちゃんとバリデーションされました。ですが、期待している型ではないnameとageはエラーになっていないようです。

ActiveRecordだったらこのままcreate!しても良い感じに保存してくれますが、バリデーションしたパラメータで何かするときには500エラーになりそうです。型をチェックするバリデータを作っても良いかもしれません。

user = User.create!(name: 15, age: '15')
user.name # "15"
user.age # 15
type_validator.rb
class TypeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value.is_a?(options[:type])
      record.errors.add(attribute, 'is invalid')
    end
  end
end
form = SampleForm.new(name: 15, mail: 3, age: '15')
form.valid? # false
form.errors.to_hash # {:name=>"is invalid", :mail=>"must be an mail address", :age=>"is invalid"}

型チェックに関してはvalidates_typeというgemもあるようです。

追記

クエリパラメータの値は文字列で受け取るので、バリデーション時点では文字列型も許容しないとバグになるかもしれません。バリデーションを通った直後に必要な型に変換した方がよさそうです。

# /users?page=1
params[:page] # "1"
form = UserForm.new(params)
form.valid? # true
params[:page] = params[:page].to_i
params[:page] # 1

allow_nil の罠(自爆)

以下のパラメータのバリデーションを考えましょう。

  • id_array
    • 数字の配列
    • 必須(nilはダメ)

presence: trueとすると空配列もエラーになってしまいます。
そこで自分はなんとなくで次のように書きました。

array_validator.rb
class ArrayValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value.nil? || value.is_a?(Array)
      record.errors.add(attribute, 'is invalid')
    end
  end
end
sample_form.rb
class SampleForm
  include ActiveModel::Model
  attr_accessor :id_array

  validates :id_array, allow_nil: false, array: true
end

さっそくnilを入れてみましょう

form = SampleForm.new(id_array: nil)
form.valid? # true

アレ?バリデーションが仕事してくれませんでした。。。

Railsガイドを読んでみると、allow_nil: trueにしたときにバリデーションをスキップするのであって、別にfalseにしたからってnilでエラーにしてくれるわけではないようです。

numericalityオプションの仕様

デフォルトでは、numericalityのnil値は許容されません。allow_nil: trueオプションでnil値を許可できます。

と同様にカスタムバリデータではnilではエラーになるようにしておいて、nilを許容する場合にallow_nil: trueを指定するのがよさそうです。


パラメータを全然違う型で送ったのにパラメータエラーにならない

配列型を期待しているsome_arrayというパラメータがあるとします。このパラメータはnullでも問題なくAPIを実行できるとします。
ここにhogeという文字列を入れてリクエストすると、以下のようななります。

# params = {
#   some_array: 'hoge'
# }

permitted_params = params.permit(
  some_array: []
).to_h

permitted_params # {}

form = Sampleform.new(permitted_params)

# some_arrayに文字列を入れてリクエストしているのでエラーになって欲しいが、permitによってなくなっているのでtrueになる
form.valid?

# some_array にデフォルト値があると、そのまま作成できる
User.create!(permitted_params)

配列であるべきところに文字列を指定したのでパラメータエラーになって欲しかったのですが、ユーザ作成に成功してしまいました。

最初に params に期待しているキーが全て存在することを確認すればいい気がしますが、自分には良い感じの実装方法が思いつきませんでした。対策を知っている方がいればコメントお願いします・・・


おわりに

期待していない型でやってくることも、nullチェックも、画面があればまず画面でバリデーションされるので今までこの問題が表面化しなかったのは画面のおかげだったんだなぁと思いました。
このバリデーションは1回共通部分として作ってしまえば使いまわせるはずなのでしっかりしたいです。


参考文献

Railsガイド

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0