DEV Community

Cover image for How to implement Form Object pattern in Ruby on Rails?
Vlad Hilko
Vlad Hilko

Posted on • Edited on

How to implement Form Object pattern in Ruby on Rails?

Overview:

In this article, we will cover the use of form objects in Ruby on Rails applications. We will explore three different approaches to implementing form objects in Rails: ActiveRecord Form Objects, Virtus Form Objects and DRY-rb Form Objects. We will provide examples and discuss the pros and cons of each approach.

Definition

In simple terms, Form Object is a design pattern that handles and validates data before saving it to the database.

Why do we need it and what problems can this pattern solve? 🤷🏻‍♂️

Let's take a look at the following example.

Imagine we have the model:

# app/models/animal.rb

class Animal < ApplicationRecord

  attribute :name, :string

  validates :name, presence: true

end
Enter fullscreen mode Exit fullscreen mode

And controller:

def create
  animal_params = params.require(:animal).permit(:name)
  animal = Animal.create(animal_params)

  render json: animal
end
Enter fullscreen mode Exit fullscreen mode

What are we doing here?

  • We accept only allowed data ( params.require(:animal).permit(:name) )
  • We convert data to the specific type ( attribute :name, :string )
  • We validate the data ( validates :name, presence: true )
  • We save this data the database (Animal.create(animal_params))
  • We render the saved record (render json: animal)

Do we have any problems with this approach? 🤔

Yes, we have.

  • We share data preparation responsibilities between controller and model
  • We violate the Single Responsibility Principle because the model becomes responsible for data preparation and validation.
  • We cannot use different validation rules for different cases within the same model.

For example, sometimes we want to have different attributes and validations depending on where the code is run, such as different validation rules for a Rake task versus a CRM system or API.

  • It would be nice to receive validation error as soon as possible to avoid potential unnecessary calculations.

How can we solve this problem? 🤔

Form objects can help address these issues by centralizing data validation in a single place. The following responsibilities from the code above actually belongs to the Form Object:

  • We accept only allowed data ( params.require(:animal).permit(:name) )
  • We convert data to the specific type ( attribute :name, :string )
  • We validate the data ( validates :name, presence: true )

Let's discuss them more closely:

Responsibilities

  • Enforcing strict attribute definitions

Form objects may have strict attribute definitions, meaning that they only allow specific attributes to be set and accessed. This can help to enforce the structure of the form data and prevent unintended changes.

  • Parsing, transforming, and sanitizing incoming data

Form objects are responsible for parsing, transforming, and sanitizing incoming data from an API request or form submission. This may involve converting data types, formatting data, or performing other data transformations. In addition, form objects may be responsible for sanitizing the incoming data to prevent malicious input or cross-site scripting attacks. By parsing, transforming, and sanitizing the incoming data, form objects can ensure that the data meets the requirements of the application and is safe to use.

  • Validating the data

Form objects are responsible for ensuring that the incoming data meets the requirements of the application. This may involve checking for required fields, validating the format of the data, and ensuring that the data is consistent with the business rules of the application.

Based on these responsibilities let's try to define the key elemets for our new interface

  • For Parsing, transforming, and sanitizing incoming data we need to add attribute :name, :type method for each attribute.
  • For Enforcing strict attribute definitions we need to add attributes method. This method will return only allowed attributes with data to be stored in the database.
  • For Validating the data we need to include validators (to set attribute validation rules) and valid! method (to validate attributes based on the rules)

Based on the above statement, let's try to build the desired interface.

class AnimalForm
  attribute :name, :string # prepare attributes

  validates :name, presence: true # set validation rules

  def valid!
    # validate attributes
  end

  def attributes
    # returns a hash with all allowed attributes and data, which is ready to be saved in the database
  end
end

form = AnimalForm.new(name: 'Name')
form.valid!

Animal.create(form.attrubutes)
Enter fullscreen mode Exit fullscreen mode

This is exactly the interface we expect to see for the Form Object.

Do we have a ready-made solution with a similar interface? 🤔

Yes, we have and we'll consider 3 independent ways to create them in Rails. Let's look at these options one by one.


Plain Ruby class based on ActiveRecord model

Our first option is to use the same method as in the model, but in a separate class to avoid violating the Single Responsibility Principle. For example:

# app/units/forms/animal.rb

module Forms
  class Animal

    include ActiveModel::Model
    include ActiveModel::Attributes
    include ActiveModel::Validations

    attribute :name, :string

    validates :name, presence: true

  end
end

form = Forms::Animal.new(name: 'Cat')
form.validate! # true
form.attributes # {'name'=>'Cat'}
Enter fullscreen mode Exit fullscreen mode

I like this interface, and we don't need to add any external gems, but I've never used this approach in production code, so I don't know if there are any hidden problems 😅


Virtus Form Object

The second option is to use Virtus gem. For example:

# app/units/forms/animal.rb

module Forms
  class Animal

    include Virtus.model
    include ActiveModel::Validations

    attribute :name, String

    validates :name, presence: true

  end
end

form = Forms::Animal.new(name: 'Cat')
form.validate! # true
form.attributes # {:name=>"Cat"}

Enter fullscreen mode Exit fullscreen mode

We've been using this gem in our production code for a long time and it works perfectly fine. However, the biggest drawback is that the gem is no longer supported and has been migrated to dry-rb.


Dry-rb Form Object

Our third option to create Form Object is dry-rb gems. To be more precise, we are going to use the dry-validation gem. Let's take a look at the example below:

# app/units/forms/animal.rb

module Forms
  class Animal < Dry::Validation::Contract

    params do
      required(:name).filled(:string)
    end

    rule(:name) do
      key.failure('must be present') if value.blank?
    end

  end
end


form = Forms::Animal.new
form.call(name: 'Cat') # validate!
form.call(name: 'Cat').values.data # return attributes '{:name=>"Cat"}'
Enter fullscreen mode Exit fullscreen mode

Honestly, I don't like this interface and haven't had enough time to get used to it and fully explore it 😅 However, it is still worth trying as the most modern approach.


The final solution may look like this and be placed under the Service object:

def create
  form = Forms::Animal.new(params)
  form.validate!

  animal = Animal.create(form.attributes)

  render json: animal
end
Enter fullscreen mode Exit fullscreen mode

P.S. You can read the following article to understand how Form and Service objects can work together

Conclusion

In conclusion, Form Object is a useful design pattern in Ruby on Rails applications for managing and validating data. The advantages of using form objects are numerous, including the most important ones:

  • Improves code readability and maintainability:

Form objects can improve the readability and maintainability of the code by allowing form-related logic to be organized in a single place.

  • Decouples form data from model data

Form objects can decouple the form data from the model data, which can make it easier to make changes to the form without affecting the underlying model.

  • Improves testability

Form objects can be tested in isolation from the rest of the application, which can make it easier to test the form-related logic and ensure that it is working correctly.

  • Improves security

Form objects can improve the security of an application by providing a central place to handle input validation and sanitization.

  • Reusability

Allows for reuse of form logic by using form objects in multiple places within the application

  • Extensibility

We can create as many Form Objects as we want to cover all possible cases for one model. For example: Forms::AnimalImport, Forms::AnimalCRM and Forms::AnimalAPI.

Top comments (0)