Validating Models Without ActiveRecord in Rails
Have you ever needed a class in Rails that behaves like an ActiveRecord model but isn’t backed by a database table? A common scenario is building something like a contact form. You need validation for fields like name, email, and message, but there’s no need to store this data in your database.
Sure, there are gems for this, but sometimes it’s satisfying to understand the mechanics and avoid unnecessary dependencies. Let’s explore how to validate a plain Ruby object in Rails without using ActiveRecord or plugins.
The Problem: Validations Without ActiveRecord
You might assume that including ActiveModel::Validations
in your class is
enough to handle validations, like so:
class Contact
include ActiveModel::Validations
attr_accessor :name, :email, :message
validates :email, presence: true
validates :message, presence: true
end
This setup is a good start but incomplete. ActiveModel validations rely on
certain methods that ActiveRecord provides by default, like persisted?
and
new_record?
. Without these, your validations won’t work as expected.
Let’s flesh out a functional implementation.
A Complete Example
Here’s how you can build a plain Ruby class with ActiveModel validations:
class Contact
include ActiveModel::Model
attr_accessor :name, :email, :message
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :message, presence: true
def save
if valid?
# Logic for sending an email or performing another action
true
else
false
end
end
end
Key Points:
ActiveModel::Model
: This module includes all necessary ActiveModel modules, like validations and conversion methods. It’s a one-stop shop for turning plain Ruby objects into form-like models.save
Method: You’re responsible for defining what “saving” means—e.g., sending an email or calling an external API.
Using the Model
Here’s how you might use this Contact
class:
contact = Contact.new(name: "Nico", email: "nico@test.com", message: "Hey!")
if contact.save
puts "Message sent!"
else
puts "Errors: #{contact.errors.full_messages.join(', ')}"
end
Adding Custom Behavior
Want to reuse this pattern for multiple non-ActiveRecord models? Extract a base class:
class ApplicationForm
include ActiveModel::Model
def save
if valid?
perform
true
else
false
end
end
def perform
raise NotImplementedError, "Subclasses must define `perform`"
end
end
class ContactForm < ApplicationForm
attr_accessor :name, :email, :message
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :message, presence: true
def perform
# Logic for sending the contact message
end
end
This pattern keeps your code DRY and makes it easy to add new non-database models in the future.
When to Use This Approach
- Form Objects: Perfect for forms where data doesn’t need to be persisted, like contact forms, search forms, or API integrations.
- Service Objects: Combine with service objects to encapsulate business logic while still benefiting from validations.
- Avoid Overhead: Great when you want lightweight models without the overhead of database tables.
Alternatives to Consider
If you’re looking for prebuilt solutions, consider these gems:
- Reform: Provides a robust framework for form objects with validations and composition.
- Virtus: Adds attributes and type coercion to plain Ruby objects (though now deprecated in favor of dry-rb).
- ActiveInteraction: A gem for managing business logic and validations.
Conclusion
Validating non-ActiveRecord models in Rails is straightforward with ActiveModel. By understanding the building blocks, you can create lightweight, flexible models tailored to your app’s needs. Whether you’re sending a contact email or building an external API integration, this approach keeps your code clean and focused.
Try it out in your next Rails project—you might find it’s the perfect fit for your use case!
Share on
Twitter Facebook LinkedInHave comments or want to discuss this topic?
Send an email to ~bounga/public-inbox@lists.sr.ht