This content originally appeared on DEV Community and was authored by Jordan Hudgens
If you're building a Ruby gem that integrates with Rails applications, you might want to provide custom validators that developers can use declaratively in their models. In this comprehensive tutorial, we'll walk through building a custom ActiveModel validator in a gem, using the domain_extractor gem's DomainValidator as our case study.
Why Custom Validators?
Custom validators allow gem users to validate model attributes with a simple, declarative syntax:
class User < ApplicationRecord
validates :website, domain: true
validates :company_url, domain: { allow_subdomains: true }
end
This is much cleaner than writing custom validation methods in every model, and it encapsulates complex validation logic in a reusable, testable component.
Table of Contents
- Understanding ActiveModel::EachValidator
- Creating the Validator Class
- Adding Railtie for Auto-loading
- Common Pitfalls and How to Fix Them
- Testing Your Validator
- Real-World Example: domain_extractor
1. Understanding ActiveModel::EachValidator
Rails provides ActiveModel::EachValidator as the base class for attribute-specific validators. It handles the iteration over attributes and calls your validation logic for each one.[3][4]
The key method you need to implement is validate_each(record, attribute, value):
- record: The model instance being validated
- attribute: The attribute name (as a symbol)
- value: The current value of the attribute
2. Creating the Validator Class
Let's start by creating a custom validator for domain names. Create a file at lib/your_gem/validators/domain_validator.rb:
module YourGem
module Validators
class DomainValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? && options[:allow_blank]
unless valid_domain?(value)
record.errors.add(
attribute,
options[:message] || :invalid_domain,
value: value
)
end
end
private
def valid_domain?(value)
# Your validation logic here
# For domain_extractor, this uses DomainExtractor.valid?(value)
YourGem.valid?(value)
end
end
end
end
Key Points:
- Namespace your validator under your gem's module to avoid conflicts
-
Handle blank values appropriately with
allow_blankoption -
Use custom error messages through the
options[:message]parameter - Keep validation logic separate in private methods for testability
3. Adding Railtie for Auto-loading
To make your validator automatically available in Rails applications, you need to create a Railtie. Create lib/your_gem/railtie.rb:
module YourGem
class Railtie < ::Rails::Railtie
initializer 'your_gem.validators' do
require 'your_gem/validators/domain_validator'
end
end
end
Then, in your main gem file (lib/your_gem.rb), add:
require 'your_gem/version'
require 'your_gem/railtie' if defined?(Rails)
module YourGem
# Your gem code here
end
The if defined?(Rails) check ensures the Railtie only loads when Rails is present, keeping your gem usable in non-Rails contexts.
4. Common Pitfalls and How to Fix Them
Pitfall #1: Incorrect Base Class (Version 0.2.5 Bug)
❌ Wrong:
class DomainValidator < ActiveRecord::Base
# This won't work!
end
✅ Correct:
class DomainValidator < ActiveModel::EachValidator
# This is the right base class
end
Why this matters: ActiveRecord::Base is for database-backed models, not validators. Using it as a base class for validators will cause instantiation errors when Rails tries to use your validator.
This was the exact bug in domain_extractor v0.2.5, which was quickly fixed in v0.2.6. The version 0.2.5 was yanked from RubyGems due to this critical error.
Pitfall #2: Not Handling Options Properly
Your validator should respect standard validation options:[4][3]
def validate_each(record, attribute, value)
# Always check allow_blank first
return if value.blank? && options[:allow_blank]
return if value.nil? && options[:allow_nil]
# Your validation logic
end
Pitfall #3: Forgetting to Require the Validator
Make sure your Railtie explicitly requires the validator file:
initializer 'your_gem.validators' do
require 'your_gem/validators/domain_validator'
end
5. Testing Your Validator
Create comprehensive tests for your validator:
# spec/validators/domain_validator_spec.rb
require 'spec_helper'
RSpec.describe YourGem::Validators::DomainValidator do
class TestModel
include ActiveModel::Validations
attr_accessor :website
validates :website, domain: true
end
let(:model) { TestModel.new }
describe 'valid domains' do
it 'accepts valid domain names' do
model.website = 'example.com'
expect(model).to be_valid
end
it 'accepts domains with subdomains' do
model.website = 'blog.example.com'
expect(model).to be_valid
end
end
describe 'invalid domains' do
it 'rejects invalid domain formats' do
model.website = 'not a domain'
expect(model).not_to be_valid
expect(model.errors[:website]).to be_present
end
it 'rejects domains with invalid characters' do
model.website = 'exam_ple.com'
expect(model).not_to be_valid
end
end
describe 'options' do
class TestModelWithOptions
include ActiveModel::Validations
attr_accessor :website
validates :website, domain: { allow_blank: true }
end
it 'respects allow_blank option' do
model = TestModelWithOptions.new
model.website = ''
expect(model).to be_valid
end
end
end
6. Real-World Example: domain_extractor
The domain_extractor gem provides a production-ready example of a custom validator. Let's look at how it implements domain validation:
Usage in Rails Models
class Company < ApplicationRecord
# Basic domain validation
validates :website, domain: true
# With options
validates :blog_url, domain: {
allow_blank: true,
message: 'must be a valid domain name'
}
# Multiple validations
validates :primary_domain, domain: true, presence: true
end
What It Validates
The DomainValidator in domain_extractor checks:
✅ Valid domain format (RFC-compliant)
✅ Proper TLD structure (using Public Suffix List)
✅ Multi-part TLDs (e.g., .co.uk, .com.au)
✅ Subdomain handling
✅ IP address detection
Implementation Highlights
# From domain_extractor
module DomainExtractor
module Validators
class DomainValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? && options[:allow_blank]
unless DomainExtractor.valid?(value)
record.errors.add(
attribute,
options[:message] || :invalid_domain,
value: value
)
end
end
end
end
end
The validator delegates to DomainExtractor.valid?, which performs comprehensive domain validation using the gem's core parsing logic.
Advanced Features
Supporting Multiple Validation Options
class DomainValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? && options[:allow_blank]
result = DomainExtractor.parse(value)
if options[:require_subdomain] && result.subdomain.nil?
record.errors.add(attribute, :subdomain_required)
return
end
if options[:allowed_tlds] && !options[:allowed_tlds].include?(result.tld)
record.errors.add(attribute, :invalid_tld)
return
end
unless DomainExtractor.valid?(value)
record.errors.add(attribute, options[:message] || :invalid_domain)
end
end
end
Custom Error Messages
Support I18n for error messages by adding to your gem:
# config/locales/en.yml
en:
errors:
messages:
invalid_domain: "is not a valid domain name"
subdomain_required: "must include a subdomain"
invalid_tld: "has an unsupported top-level domain"
Performance Considerations
When building validators for gems that focus on performance (like domain_extractor):
- Cache expensive operations: Parse domains once and reuse results
- Minimize allocations: Use frozen strings and constants
- Fail fast: Check simple conditions before expensive validation
- Benchmark: Validate performance impact on model save operations
class DomainValidator < ActiveModel::EachValidator
# Cache regex patterns
DOMAIN_REGEX = /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}\z/i.freeze
def validate_each(record, attribute, value)
# Quick format check before expensive parsing
return record.errors.add(attribute, :invalid_domain) unless value =~ DOMAIN_REGEX
# Now do expensive validation
unless DomainExtractor.valid?(value)
record.errors.add(attribute, :invalid_domain)
end
end
end
Integration with ActiveRecord
Your validator integrates seamlessly with ActiveRecord's validation framework:
class User < ApplicationRecord
# Combines with other validators
validates :website, domain: true, presence: true, uniqueness: true
# Conditional validation
validates :blog_url, domain: true, if: :blogger?
# On specific actions
validates :temp_domain, domain: { allow_blank: true }, on: :create
# With custom callbacks
validates :company_domain, domain: true
before_validation :normalize_domain
private
def normalize_domain
self.company_domain = company_domain&.downcase&.strip
end
end
The Version 0.2.5 → 0.2.6 Fix
The domain_extractor gem provides an excellent real-world example of catching and fixing a validator bug:
Version 0.2.5 (Yanked)
The initial implementation had a critical error:
class DomainValidator < ActiveRecord::Base # ❌ Wrong base class!
def validate_each(record, attribute, value)
# ... validation logic
end
end
Problem: This caused ArgumentError when Rails tried to instantiate the validator, because ActiveRecord::Base is for models, not validators.
Version 0.2.6 (Fix)
The fix was simple but critical:
class DomainValidator < ActiveModel::EachValidator # ✅ Correct!
def validate_each(record, attribute, value)
# ... validation logic
end
end
This demonstrates the importance of:
- Using the correct base class
- Thorough testing of Rails integration
- Quick response to issues (same-day fix and release)
- Proper gem versioning (yanking broken versions)
Complete File Structure
Here's the recommended structure for your gem with validators:
your_gem/
├── lib/
│ ├── your_gem.rb
│ ├── your_gem/
│ │ ├── version.rb
│ │ ├── railtie.rb
│ │ ├── validators/
│ │ │ └── domain_validator.rb
│ │ └── core_logic.rb
├── spec/
│ ├── validators/
│ │ └── domain_validator_spec.rb
│ └── spec_helper.rb
├── config/
│ └── locales/
│ └── en.yml
└── your_gem.gemspec
Gemspec Configuration
Don't forget to specify Rails as a development dependency:
Gem::Specification.new do |spec|
spec.name = "your_gem"
spec.version = YourGem::VERSION
spec.authors = ["Your Name"]
# Don't require Rails at runtime (optional usage)
spec.add_development_dependency "rails", ">= 5.2"
spec.add_development_dependency "rspec-rails"
# Your gem's core dependencies
spec.add_dependency "public_suffix", "~> 6.0"
end
Documentation Best Practices
Include clear examples in your README:
## Rails Integration
domain_extractor provides a custom validator for ActiveRecord models:
class User < ApplicationRecord
validates :website, domain: true
end
### Options
- `allow_blank`: Skip validation for blank values
- `message`: Custom error message
- `allow_subdomains`: Require or allow subdomains
validates :website, domain: {
allow_blank: true,
message: "must be a valid domain"
}
Why This Matters for OpenSite AI
The domain_extractor gem is part of OpenSite AI's open-source initiative, focused on:
- Increasing organic traffic: High-quality, well-documented gems attract users and backlinks
- Performance-first design: Every feature, including validators, is optimized for speed
- Developer experience: Clean APIs and Rails integration make adoption easy
- Community building: Open source contributions grow the ecosystem
Key Takeaways
-
Use
ActiveModel::EachValidatoras your base class for attribute validators - Create a Railtie to auto-load validators in Rails applications
-
Handle standard options like
allow_blankand custom messages - Test thoroughly including Rails integration tests
- Keep validation logic separate from the validator class for reusability
- Document clearly with examples in your README
- Version carefully and be ready to yank broken releases
Try It Yourself
Install domain_extractor and see the validator in action:
gem install domain_extractor
Or add to your Gemfile:
gem 'domain_extractor'
Then use it in your models:
class Company < ApplicationRecord
validates :website, domain: true
end
Resources
- domain_extractor on GitHub
- domain_extractor on RubyGems
- ActiveModel::EachValidator Documentation
- Rails Guides: Custom Validators
- OpenSite AI
Conclusion
Building custom validators for your Ruby gems enhances their value to Rails developers. By following the patterns demonstrated in domain_extractor, you can create powerful, reusable validation logic that integrates seamlessly with Rails' validation framework.
The key is using the right base class (ActiveModel::EachValidator), providing a Railtie for auto-loading, and thoroughly testing your implementation. Learn from real-world examples like the 0.2.5 → 0.2.6 fix to avoid common pitfalls.
Ready to build your own validator? Start by studying the domain_extractor source code and adapt the patterns to your gem's needs.
About OpenSite AI: We're building high-performance, open-source tools for Ruby developers. Check out our other open source projects and join our growing community!
This content originally appeared on DEV Community and was authored by Jordan Hudgens
Jordan Hudgens | Sciencx (2025-11-16T11:49:21+00:00) Building Custom Ruby on Rails Model Validators in Gems: A Complete Guide. Retrieved from https://www.scien.cx/2025/11/16/building-custom-ruby-on-rails-model-validators-in-gems-a-complete-guide/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.