Tuesday, February 8, 2011

The type casting edge case in Rails validation

Validation is one of the things Rails does very well. Most of the time you can manage your validation with one of the standard declarative validations. Occasionally you need something custom, but even then life is good and your custom validation is as simple as writing a two or three line method. However, there are edge cases and I'm going to talk about one. Let's start with the request. Request parameters come in to your application as strings and rails automatically maps them to the type prescribed by your DB.

If you aren't thinking about this (and it is easy not to think about it) you can be in for some surprises. If your field is an integer, but a user supplies a non numeric string, it will get type cast as zero. If your field is a date, but a user supplies an invalid date, like February 31, you will get March 3rd. If they were really off and supplied February 43rd, you will just get nil.

This leads to some confusing behavior. Imagine you have an optional date of birth field on a form and a manual validation to ensure that date is in the past. If somebody enters February 43rd 1969 as their date because of type casting the field is set to nil and nothing will happen. Which is weird.

If we want to give our users meaningful feedback about their dates we are going to have to do something about this. Looking at stackoverflow. Most people trying to solve this problem just override the accessor. Bad! Changing the behavior of an accessor in the best case is introducing unexpected behavior to your models and in the worst case will cause bugs because that isn't how a standard activerecord object works.

The Rails folks have already considered this problem and so there is a feature that works around it. It seems not too many people know about it, but it is documented! ActiveRecord has alternate versions of every attribute. They are aptly named *_before_type_cast. So in our case date_of_birth_before_type_cast would contain the string 2/43/1969. And you can use that value to give a more accurate error message. I lost a couple hours the other day researching this problem, but lesson learned and I thought I would spread the good word about _before_type_cast.

No comments: