[Ruby] Preventing booleans from being arbitrarily cast by the Rails API and passing validation

1 minute read

Event

When creating or updating a column of boolean type with Rails API, I want to play any type other than 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

If you do this, you should be able to play anything other than booleans…

$ 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"}}

Specify “moge” for boolean opened column

Expectation: It is an error because it is not a boolean Actual condition: No error occurs, cast to true and saved

$ 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"}}

Expectation: It is an error because it is not a boolean Actual condition: No error occurs, cast to false and saved

Apparently the value will be passed in the cast state

Countermeasures

Make a custom validation

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

*Restart the Rails server after implementing custom validation

Result

$ 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"}}

As expected, all but true and false are now played

Reference

Method to validate boolean type column with Rails and return as error if the type is different I am writing this article because if you implement it with the above URL, it will get caught in rubocop in various ways