Validates Type 2.0
29 Nov 2015One of the first projects that I worked on during this Year of Commits was validates_type. In case that name is too obscure, validates_type
is a gem for validating that a specific value is exactly the type it is expected to be. The validates_type
library is compatible to ActiveModel
style validations.
Originally, the validates_type
gem was very strict with what it validated against. It started with basic included Ruby types like Integer
, Float
, and String
. This was a fine first step but proved too restrictive for basically any use case.
In a recent update, the validates_type
gem has been extended to validate against any defined type in a system. With this update, the uses of this gem greatly increased. They increased so much that it is possible more than 2 people will use it, and that would be pretty awesome.
Still Valid
The validates_type
gem can be useful for a system that cares about exact types at save time. For instance, a model that has an incorrect database column type (because of legacy or other reasons) can still control validation over its data just the same.
If a “junior developer who is now somehow the CTO” created the original system. And if that developer did something like create an is_published
column on the authors
table set as a varchar
, validates_type
makes that less of an issue.
class Author < ActiveRecord::Base
validates_type :is_published, :boolean
end
If someone tries to assign a nonsense value to the is_published
attribute, validates_type
will ensure it does not save.
Note: callback skipping methods will still save bad values, just like any other ActiveRecord
validators.
> author = Author.new
# => #<Author id: nil, name: nil, created_at: nil, updated_at: nil, is_published: nil>
> author.is_published = 'foo'
# => "foo"
> author.save!
# => ActiveRecord::RecordInvalid: Validation failed: is_published is expected to be a Boolean and is not.
This way, there is not a bunch of random data in the authors
table because of a bad decision made a while ago. More examples on how this gem can be used are found at the prequel to this post and the project’s README.
The New Hotness
The 2.0 update brought flexibility to validates_type
. This enables greater control over serialized attributes on ActiveRecord
objects.
In a system with an intermediary class inserted between the column assignment and the column serialization, this extension can show its true value.
In an application that deals with Books
, each book must store information regarding how it was published:
class Book < ActiveRecord::Base
serialize :publishing_info, PublishingInformation
end
Perhaps each book can have such a variety of publishing information, or the data present can vary widely between each book. Whatever the reason, we can assume that the PublishingInformation
class handles the input and extraction of the JSON
data pertinent to a specific Book
.
With that assumption, an issue could arise if another type of object was assigned to a Book
’s publishing_information
column.
> book = Book.new
> book.publishing_information = { foo: :bar }
> book.save!
# => UPDATE "books" SET "publishing_information" = $1, "updated_at" = $2 WHERE "books"."id" = $3 [["publishing_information", "{}"], ["updated_at", "2015-11-30 05:04:09.480707"], ["id", 3]]
If, like the above example, the PublishingInformation
class is forgiving, this will silently fail. The book.publishing_information
will not be set to the correct value.
On the other hand, if the PublishingInformation
does fail, it might not do so in an obvious way.
However, with validates_type
, this issue becomes explicit.
Adding the type validation to the book model:
class Book < ActiveRecord::Base
serialize :publishing_info, PublishingInformation
validates_type :publishing_info, PublishingInformation
end
Then the same save!
call gives a different error:
> b = Book.new
> b.publishing_information = { a: :b }
> b.save!
# => ActiveRecord::RecordInvalid: Validation failed: publishing_information is expected to be a PublishingInformation and is not.
Now, a readable error is produced and nothing is left to the imagination.
A Reasonable Start
Was the used example extremely specific? Yes. But, I would wager that at least one application out there has had a problem similar to this and validates_type
can ensure that it never happens again. Hopefully, this example and the problem it did end up solving with at least act as inspiration for this library’s other uses out in the wild.
I have found a use for this type of validation and hope others can as well. If a use case arises that this gem does not support but could easily, I invite everyone to contribute to it.