A fluent, type-safe schema validation library for Ruby inspired by Zod. Build complex validations with chainable methods, custom error messages, and built-in transformations.
✨ Fluent API - Chain validators for readable, expressive validation
🎯 Custom Error Messages - Override default messages for better UX
🔄 Transformations - Transform values during validation (trim, lowercase, etc.)
📦 Zero Dependencies - Lightweight and fast
âś… 100% Test Coverage - Reliable and production-ready
Add to your Gemfile:
gem 'validator_rb'Or install directly:
gem install validator_rbrequire 'validator_rb'
# Basic validation
validator = ValidatorRb.string.min(3).max(50)
result = validator.validate("hello")
result.success? # => true
result.value # => "hello"
result.success? # => true
result.value # => "hello"
result.errors # => [] # Array of ValidatorRb::ValidationError objects
# With transformations
validator = ValidatorRb.string.trim.lowercase.email
result = validator.validate(" USER@EXAMPLE.COM ")
result.success? # => true
result.value # => "user@example.com"# Minimum length
ValidatorRb.string.min(5).validate("hello")
# Maximum length
ValidatorRb.string.max(10).validate("hello")
# Exact length
ValidatorRb.string.length(5).validate("hello")
# Combine constraints
ValidatorRb.string.min(3).max(50).validate("hello")# Email validation
ValidatorRb.string.email.validate("user@example.com")
# URL validation
ValidatorRb.string.url.validate("https://example.com")
# Custom regex pattern
ValidatorRb.string.regex(/\A[A-Z]+\z/).validate("HELLO")
# Alphanumeric only
ValidatorRb.string.alphanumeric.validate("abc123")
# Letters only
ValidatorRb.string.alpha.validate("hello")
# Numbers only
ValidatorRb.string.numeric_string.validate("12345")# Not empty or whitespace
ValidatorRb.string.non_empty.validate("hello")
# Starts with prefix
ValidatorRb.string.starts_with("hello").validate("hello world")
# Ends with suffix
ValidatorRb.string.ends_with(".com").validate("example.com")# Required field (fails on nil or empty string)
validator = ValidatorRb.string.email.required
result = validator.validate(nil)
result.success? # => false
result.success? # => false
result.errors.first.message # => "is required"
result.errors.first.code # => :required
# Optional field (default - allows nil)
validator = ValidatorRb.string.email.optional
result = validator.validate(nil)
result.success? # => true# Minimum value
ValidatorRb.integer.min(0).validate(5)
# Maximum value
ValidatorRb.integer.max(100).validate(50)
# Between (inclusive)
ValidatorRb.integer.between(1, 10).validate(5)
# Comparison
ValidatorRb.integer.greater_than(0).validate(5)
ValidatorRb.integer.less_than(100).validate(50)# Positive (> 0)
ValidatorRb.integer.positive.validate(10)
# Negative (< 0)
ValidatorRb.integer.negative.validate(-5)
# Non-negative (>= 0)
ValidatorRb.integer.non_negative.validate(0)
# Non-positive (<= 0)
ValidatorRb.integer.non_positive.validate(-10)# Multiple of
ValidatorRb.integer.multiple_of(5).validate(25)
# Even numbers
ValidatorRb.integer.even.validate(4)
# Odd numbers
ValidatorRb.integer.odd.validate(7)# Coerce from string
validator = ValidatorRb.integer.coerce.min(100)
result = validator.validate("150")
result.success? # => true
result.value # => 150
# Coerce from float (truncates)
result = validator.validate(123.7)
result.value # => 123# Age validation
age_validator = ValidatorRb.integer
.min(0, message: "Age cannot be negative")
.max(150, message: "Invalid age")
.required
# Rating system (1-5 stars)
rating_validator = ValidatorRb.integer.between(1, 5).required
# Quantity (positive integers only)
quantity_validator = ValidatorRb.integer.positive.required
# Page number validation
page_validator = ValidatorRb.integer.coerce.positive
.required
# Discount percentage (0-100)
discount_validator = ValidatorRb.integer
.min(0)
.max(100)
.optional
# Minimum items
ValidatorRb.array.min_items(1).validate([1, 2, 3])
# Maximum items
ValidatorRb.array.max_items(5).validate([1, 2, 3])
# Exact length
ValidatorRb.array.length(3).validate([1, 2, 3])
# Not empty
ValidatorRb.array.non_empty.validate([1])# Unique elements
ValidatorRb.array.unique.validate([1, 2, 3])
# Contains specific element
ValidatorRb.array.contains("admin").validate(["user", "admin"])
ValidatorRb.array.includes(1).validate([1, 2, 3])Validate each element in the array using another validator:
# Array of strings
ValidatorRb.array.of(ValidatorRb.string).validate(["a", "b"])
# Array of positive integers
ValidatorRb.array.of(ValidatorRb.integer.positive).validate([1, 2, 3])
# Nested arrays
inner = ValidatorRb.array.of(ValidatorRb.integer)
ValidatorRb.array.of(inner).validate([[1, 2], [3, 4]])# Compact (remove nil)
validator = ValidatorRb.array.compact
result = validator.validate([1, nil, 2])
result.value # => [1, 2]
# Flatten
validator = ValidatorRb.array.flatten
result = validator.validate([[1, 2], [3, 4]])
result.value # => [1, 2, 3, 4]
# Trim whitespace
validator = ValidatorRb.string.trim
result = validator.validate(" hello ")
result.value # => "hello"
# Convert to lowercase
validator = ValidatorRb.string.lowercase
result = validator.validate("HELLO")
result.value # => "hello"
# Convert to uppercase
validator = ValidatorRb.string.uppercase
result = validator.validate("hello")
result.value # => "HELLO"
# Chain transformations (applied in order)
validator = ValidatorRb.string.trim.lowercase.min(3)
result = validator.validate(" HELLO ")
result.success? # => true
result.value # => "hello"Override default error messages for better user experience:
# Default message
validator = ValidatorRb.string.min(5)
result = validator.validate("hi")
result.errors.first.message # => "must be at least 5 characters"
result.errors.first.code # => :too_short
# Custom message
validator = ValidatorRb.string.min(5, message: "Password too short!")
result = validator.validate("hi")
result.errors.first.message # => "Password too short!"
result.errors.first.code # => :too_short
# Works with all validators
ValidatorRb.string.email(message: "Invalid email address")
ValidatorRb.string.url(message: "Must be a valid URL")
ValidatorRb.string.regex(/\A[A-Z]+\z/, message: "Must be uppercase letters only")Pre-configured validator combinations for common use cases:
# Non-empty string (required + trim + non_empty)
validator = ValidatorRb.string.non_empty_string
result = validator.validate(" hello ")
result.value # => "hello"
# Trimmed email (trim + lowercase + email)
validator = ValidatorRb.string.trimmed_email
result = validator.validate(" USER@EXAMPLE.COM ")
result.value # => "user@example.com"validator = ValidatorRb.string
.trim # Remove whitespace
.lowercase # Convert to lowercase
.min(3) # At least 3 chars
.max(50) # At most 50 chars
.alphanumeric # Only letters and numbers
.starts_with("user") # Must start with "user"
.required # Cannot be nil
result = validator.validate(" USER12345 ")
result.success? # => true
result.value # => "user12345"validator = ValidatorRb.string.min(10).max(5).email
result = validator.validate("hi")
if result.failure?
puts "Validation failed:"
result.errors.each do |error|
puts " - Message: #{error.message}"
puts " - Code: #{error.code}"
puts " - Path: #{error.path}"
end
# Output:
# - Message: must be at least 10 characters
# - Code: :too_short
# - Path: []
# ...
# Or get formatted message (backward compatibility)
puts result.error_message
# => "must be at least 10 characters, must be at most 5 characters, must be a valid email"
endresult = validator.validate(value)
result.success? # Boolean: true if validation passed
result.failure? # Boolean: true if validation failed
result.errors # Array<ValidationError>: list of error objects
result.error_message # String: errors joined by ", "
result.value # Object: transformed value (or original if no transformations)
### ValidationError Object
error = result.errors.first
error.message # String: Human-readable error message
error.code # Symbol: Machine-readable error code (e.g., :too_short, :invalid_email)
error.path # Array: Path to the invalid field (default: [])
error.meta # Hash: Additional context (default: {})
error.to_h # Hash: Serialized error representationAfter checking out the repo, run:
bundle install
bundle exec rspecTo run tests with coverage:
bundle exec rspec --format documentation- Fork the repository
- Create your feature branch (
git switch -c feat/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
bundle exec rspec) - Commit your changes (
git commit -am 'feat: Add amazing feature') - Push to the branch (
git push origin feat/amazing-feature) - Open a Pull Request
Bug reports and pull requests are welcome on GitHub!
- Additional validators (float, boolean, hash)
- Async validation support
- Conditional validations
- Custom validator registration
- I18n support for error messages
- JSON schema export
The gem is available as open source under the terms of the MIT License.
Inspired by Zod (TypeScript) and modern validation libraries.