How ActiveRecord casts params before validation
Jan 10, 2014 · 3 minute readI was building a model recently to support a form where users can create events with a given day and time. I was experimenting with using Postgres’ range data types and needed to parse the user input from many fields into a single tsrange (timestamp range) field. Using attr_accessors got me most of the way there but I learned some interesting things about how ActiveRecord casts data types before validation.
The data being passed from the controller looks like this:
{ :user_id => 1,
:title => "party!",
:description => "new years",
:venue => "my house",
:address => "down the street 4",
:start_date => '2014-01-20',
:start_hour => '18',
:end_hour => '23' }
and the table looks like this:
create_table "events", force: true do |t|
t.integer "user_id"
t.string "title"
t.text "description"
t.string "venue"
t.string "address"
t.datetime "created_at"
t.datetime "updated_at"
t.tsrange "when"
end
We’re interested in 1) taking the start_date, start_hour, and end_hour parameters
and 2) validating the start_hour and end_hour params are valid (between 0 and
23 in this case) so we can produce a single when
value. Notice that we want to validate the start_hour and end_hour as
integers and not strings but the parameters come from the request as strings.
One solution is to just validate the hours as strings, which would produce the following
type of validation
validates :start_hour, presence: true, inclusion: { in: ('0'..'23') }
While this validation gets the job done it doesn’t feel right to validate numbers as strings. So, we’ll need to convert the parameters first. At first, this conversion didn’t make sense to me because normally no conversion is necessary before validations. The reason we need to do this conversion by hand is because the hours attributes are virtual attributes and as such are not defined in the schema. As a result, Rails has no knowledge about the data type.
When an attribute is defined in the schema (and has a data type defined in the database) this attribute’s data type is cached by Rails upon initialization and the parameters from the request are converted to this data type before validation. In this way, if you have an integer column in the database you can safely run model validations using integers without having to first manually cast the data. Rails’ knowledge of the database types comes from the db adapter in ActiveRecord. In our case, we’re using postgres so that code can be found in ‘activerecord-4.0.2/lib/active_record/connection_adapters/postgresql_adapter.rb’. Thanks to these slides for pointing this out.
Back to our world, where we have virtual attributes, one way to solve the casting issue is to use a before_validation call.
class Event < ActiveRecord::Base
attr_accessor :start_hour,
:end_hour
before_validation :cast_hours
validates :start_hour, presence: true, inclusion: { in: (0..23) }
validates :end_hour, presence: true, inclusion: { in: (0..23) }
# other code...
private
def cast_hours
self.start_hour = start_hour.to_i
self.end_hour = end_hour.to_i
end
# other code...
end
Using this code our validations can now validate on integers instead of strings so we end up with the following:
validates :start_hour, presence: true, inclusion: { in: (0..23) }
It’s always nice to run into cases where you realize another thing that Rails has been doing for you all along. Please let me know if you have any comments or tips on better ways to handle this sort of situation.