Rails APIでbooleanが勝手にcastされバリデーション通過するのを防ぐ


事象

Rails APIでboolean型のカラムに対しcreate, updateした際、boolean以外の型は弾くようにしたい

$ rails -v
Rails 6.0.3.2
$ rails g model Post body:text opened:boolean
app/models/post.rb
class Post < ApplicationRecord
  validates :opened, inclusion: { in: [true, false]}
end

こうすれば本来、boolean以外は弾かれる、はずが…

$ curl localhost:3000/posts -X POST -H "Content-Type: application/json" -d '{"body": "hoge", "opened": "moge"}'
{"status":"success","data":{"id":1,"body":"hoge","opened":true,"created_at":"2020-08-16T01:31:14.277Z","updated_at":"2020-08-16T01:31:14.277Z"}}

booleanのopenedカラムに対し"moge"を指定

期待: booleanじゃないのでエラーになり弾かれる
実態: エラーにならず、trueにcastされsaveされる

$ curl localhost:3000/posts -X POST -H "Content-Type: application/json" -d '{"body": "hoge", "opened": "0"}'
{"status":"SUCCESS","data":{"id":2,"body":"hoge","opened":false,"created_at":"2020-08-16T01:31:28.498Z","updated_at":"2020-08-16T01:31:28.498Z"}}

期待: booleanじゃないのでエラーになり弾かれる
実態: エラーにならず、falseにcastされsaveされる

どうやら値がキャストされた状態で渡ってしまう模様

対策

カスタムバリデーションを作る

app/validators/boolean_validator.rb
class BooleanValidator < ActiveModel::EachValidator
  def validate_each(record, attr, _value)
    before_value = record.send("#{attr}_before_type_cast")
    record.errors.add(attr, "is invalid") unless %w[true false].include?(before_value.to_s.downcase)
  end
end
app/models/post.rb
class Post < ApplicationRecord
  validates :opened, boolean: true
end

※カスタムバリデーションを実装した後はRailsサーバを再起動すること

結果

$ curl localhost:3000/posts -X POST -H "Content-Type: application/json" -d '{"body": "hoge", "opened": "moge"}' 
{"status":"error","data":{"opened":["is invalid"]}}
$ curl localhost:3000/posts -X POST -H "Content-Type: application/json" -d '{"body": "hoge", "opened": "0"}' 
{"status":"error","data":{"opened":["is invalid"]}}
$ curl localhost:3000/posts -X POST -H "Content-Type: application/json" -d '{"body": "hoge", "opened": "true"}'
{"status":"success","data":{"id":3,"body":"hoge","opened":true,"created_at":"2020-08-16T02:25:18.211Z","updated_at":"2020-08-16T02:25:18.211Z"}}
$ curl localhost:3000/posts -X POST -H "Content-Type: application/json" -d '{"body": "hoge", "opened": "false"}'
{"status":"success","data":{"id":4,"body":"hoge","opened":false,"created_at":"2020-08-16T02:25:30.700Z","updated_at":"2020-08-16T02:25:30.700Z"}}

期待した通り、trueとfalse以外は弾かれるようになりました

参考

Railsでboolean型のカラムのvalidationを行い型が異なる場合エラーとして返す方法

上記URLで実装すると、rubocopにいろいろ引っかかるので今回の記事を書いています