Validates Type 2.0

One 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.