Skip to main content

Implementing attr_checked from Metaprogramming Ruby

Published: June 1, 2017 Updated: June 18, 2026 Larry Qu 3 min read

What Is attr_checked?

The attr_checked pattern from Metaprogramming Ruby dynamically defines attribute accessors with validation. Instead of hand-writing setter methods for every attribute, you declare the validation rule alongside the attribute name, and Ruby generates the getter and setter at class-load time.

This pattern is useful in domain models where attributes have business-rule constraints — age must be 18+, email must match a regex, price must be positive — and you want to avoid repetitive boilerplate.

How the Pattern Works

The implementation relies on two metaprogramming techniques: modules with callbacks and dynamic method definition.

When a class includes CheckedAttributes, the included callback fires and extends the class with ClassMethods. This makes attr_checked available as a class-level macro, similar to Ruby’s built-in attr_accessor. The macro calls define_method to create a customized setter at runtime, capturing the validation block in a closure.

Implementation Walkthrough

The first version uses a conditional check in the setter:

module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attr_name, &validation)
      define_method("#{attr_name}=") do |value|
        result = validation.call value
        if result
          @attr_name = value
        else
          raise "value is not valid"
        end
      end
      attr_reader attr_name.to_sym
    end
  end
end

The second version raises immediately on invalid input and uses instance_variable_set/instance_variable_get instead of relying on attr_reader:

module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
      define_method("#{attribute}=") do |value|
        raise 'Invalid attribute' unless validation.call(value)
        instance_variable_set("@#{attribute}", value)
      end

      define_method attribute do
        instance_variable_get "@#{attribute}"
      end
    end
  end
end

The second approach is more direct: it manages the instance variable explicitly rather than delegating to attr_reader. This gives you finer control over default values and initialization.

Usage Example

class Person
  include CheckedAttributes

  attr_checked :age do |v|
    v >= 18
  end
end

me = Person.new
p me.age = 39  # OK
p me.age = 39  # OK
p me.age = 12  # not OK — raises RuntimeError

The validation block receives the incoming value and must return a truthy or falsy result. You can replace the block with any callable, making validators reusable across attributes.

Beyond Basic Validation

Extend the pattern with composeable validators:

module Validators
  Age = ->(v) { v.is_a?(Integer) && v >= 18 && v <= 120 }
  Email = ->(v) { v =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
end

class Employee
  include CheckedAttributes
  attr_checked :age, &Validators::Age
  attr_checked :email, &Validators::Email
end

For production code, consider extracting the validation logic into a dedicated validation object or using Rails Active Model validations. The attr_checked pattern works best when you need lightweight, self-contained attribute constraints without pulling in a full validation framework.

Resources

Comments

👍 Was this article helpful?