Introduction

Sometimes you want a class that behaves like a Rails model (validations, error messages, the whole feel) without a database table behind it. A contact form is the classic case: you validate name, email and message, then send an email; there’s nothing to persist.

There are gems for this, but the mechanics are simple enough that it’s worth knowing how to do it with just ActiveModel.

What ActiveModel gives you

Including ActiveModel::Validations is already enough to validate:

class Contact
  include ActiveModel::Validations
  attr_accessor :name, :email, :message

  validates :email, presence: true
  validates :message, presence: true
end

contact = Contact.new
contact.valid?                 # => false
contact.errors.full_messages   # => ["Email can't be blank", "Message can't be blank"]

Validations work here without any ActiveRecord method like persisted? or new_record?. What you don’t get with ActiveModel::Validations alone is convenient initialization (Contact.new(name: "Nico") raises, since there’s no hash-accepting constructor), nor the naming and conversion that form helpers rely on.

That’s exactly what ActiveModel::Model adds on top.

A complete example

ActiveModel::Model bundles validations together with hash initialization, naming and conversion, everything a form-like object needs:

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
    return false unless valid?

    # send an email, call an API, whatever "saving" means here
    true
  end
end

You decide what save means; validation gives you the guard rail, and the object plugs into form_with just like an ActiveRecord model.

contact = Contact.new(name: "Nico", email: "nico@example.com", message: "Hey!")

if contact.save
  puts "Message sent!"
else
  puts "Errors: #{contact.errors.full_messages.join(', ')}"
end

A reusable base

If you’ll have several of these, extract a small base class:

class ApplicationForm
  include ActiveModel::Model

  def save
    return false unless valid?

    perform
    true
  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
    # send the contact message
  end
end

Each form declares its fields and validations, and only fills in perform.

When to use it

  • Form objects where the data isn’t persisted: contact forms, search forms, API payloads.
  • Alongside a service object, to validate input before running business logic.
  • Any time you want a lightweight, validatable object without a table behind it.

Alternatives

If you’d rather not hand-roll it, a few gems cover the same ground:

  • Reform — form objects with validations and composition.
  • dry-validation — schema-plus-rules validation (and the modern successor to the now-deprecated Virtus).
  • ActiveInteraction — business logic with built-in validations.

Wrapping up

ActiveModel already contains everything you need to validate a plain Ruby object. Reach for ActiveModel::Validations when you only want the checks, and ActiveModel::Model when you want an object that also initializes from a hash and works with form helpers. No table, no extra dependency required.

Have comments or want to discuss this topic?

Send an email to ~bounga/public-inbox@lists.sr.ht