Validations
CQL provides a comprehensive validation system that ensures data integrity before records are saved to the database. The validation system is built on a predicate-based approach that leverages Crystal's type system for compile-time safety.
Basic Validation Syntax
Define validations using the validate macro with field names and validation predicates:
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context UserDB, :users
  property id : Int32?
  property name : String
  property email : String
  property age : Int32 = 0
  property password : String?
  @[DB::Field(ignore: true)]
  property password_confirmation : String?
  # Define validations with predicates and custom messages
  validate :name, presence: true, size: 2..50, message: "Name is invalid"
  validate :email, required: true, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, message: "Email format is invalid"
  validate :age, gt: 0, lt: 120, message: "Age must be between a reasonable range"
  validate :password_confirmation, presence: true, message: "Password confirmation is required"
  def initialize(@name : String, @email : String, @age : Int32 = 0, @password : String? = nil, @password_confirmation : String? = nil)
  end
endAvailable Validation Predicates
CQL provides a rich set of built-in validation predicates:
Presence and Required
- presence: true: Ensures the field is not nil and not empty
- required: true: Ensures the field is not nil
validate :name, presence: true      # Must not be nil or empty
validate :user_id, required: true   # Must not be nil (but can be empty)Numeric Comparisons
- gt: value: Greater than
- gte: value: Greater than or equal to
- lt: value: Less than
- lte: value: Less than or equal to
- eq: value: Equal to
validate :age, gt: 0, lt: 150                    # Age between 1 and 149
validate :score, gte: 0, lte: 100               # Score between 0 and 100
validate :priority, eq: 1                       # Exact value matchSize Validations
- size: number: Exact size
- size: range: Size within range
validate :username, size: 3..20                 # Between 3 and 20 characters
validate :zip_code, size: 5                     # Exactly 5 charactersPattern Matching
- match: regex: Must match regular expression
validate :email, match: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validate :phone, match: /\A\d{3}-\d{3}-\d{4}\z/Inclusion and Exclusion
- in: array: Value must be in the array
- in: range: Value must be in the range
- exclude: array: Value must not be in the array
- exclude: range: Value must not be in the range
validate :status, in: ["active", "inactive", "pending"]
validate :rating, in: 1..5
validate :username, exclude: ["admin", "root", "system"]Custom Messages
Provide custom error messages for better user experience:
class Product
  include CQL::ActiveRecord::Model(Int32)
  db_context StoreDB, :products
  property id : Int32?
  property name : String
  property price : Float64
  property category : String
  validate :name, presence: true, message: "Product name cannot be blank"
  validate :price, gt: 0, message: "Price must be greater than zero"
  validate :category, in: ["electronics", "books", "clothing"], message: "Invalid product category"
  def initialize(@name : String, @price : Float64, @category : String)
  end
endWorking with Validation Errors
Checking if a Record is Valid
user = User.new("", "invalid-email", -5)
# Check validity
if user.valid?
  puts "User is valid"
else
  puts "User has validation errors"
endAccessing Validation Errors
user = User.new("", "invalid-email", -5)
unless user.valid?
  errors = user.errors
  # Get all error messages
  error_messages = errors.map(&.message)
  puts error_messages
  # => ["Name is invalid", "Email format is invalid", "Age must be between a reasonable range"]
  # Access individual errors
  errors.each do |error|
    puts "Field: #{error.field}, Message: #{error.message}"
  end
endValidating with Context
You can validate with specific contexts for different scenarios:
class User
  include CQL::ActiveRecord::Model(Int32)
  # Validations can be context-specific
  validate :password, presence: true, on: :create
  validate :current_password, presence: true, on: :update
end
user = User.new("John", "john@example.com")
# Validate for specific context
user.valid?(:create)    # Checks password presence
user.valid?(:update)    # Checks current_password presence
user.valid?             # Runs all validations regardless of contextValidation Exceptions
Force validation and raise an exception if invalid:
user = User.new("", "invalid-email", -5)
begin
  user.validate!  # Raises ValidationError if invalid
rescue CQL::ActiveRecord::Validations::ValidationError => e
  puts "Validation failed: #{e.message}"
  # Message contains comma-separated error messages
endCustom Validators
Create custom validators for complex validation logic:
class PasswordValidator < CQL::ActiveRecord::Validations::CustomValidator
  def valid? : Array(CQL::ActiveRecord::Validations::Error)
    errors = [] of CQL::ActiveRecord::Validations::Error
    password = @record.password
    # Check minimum length
    unless password && password.size >= 8
      errors << CQL::ActiveRecord::Validations::Error.new(:password, "Password must be at least 8 characters")
    end
    # Check for mixed case
    unless password && password.matches?(/[a-z]/) && password.matches?(/[A-Z]/)
      errors << CQL::ActiveRecord::Validations::Error.new(:password, "Password must contain both uppercase and lowercase letters")
    end
    # Check for numbers
    unless password && password.matches?(/\d/)
      errors << CQL::ActiveRecord::Validations::Error.new(:password, "Password must contain at least one number")
    end
    errors
  end
end
class User
  include CQL::ActiveRecord::Model(Int32)
  db_context UserDB, :users
  property id : Int32?
  property name : String
  property email : String
  property password : String?
  # Use custom validator
  use PasswordValidator
  validate :name, presence: true, size: 2..50, message: "Name must be between 2 and 50 characters"
  def initialize(@name : String, @email : String, @password : String? = nil)
  end
endValidation Best Practices
1. Use Appropriate Predicates
# Good - specific predicates
validate :age, gt: 0, lt: 120
validate :email, required: true, match: EMAIL_REGEX
# Avoid - overly complex custom validators for simple cases
validate :age, custom: :validate_age_range2. Provide Clear Error Messages
# Good - descriptive messages
validate :email, required: true, message: "Email address is required for account creation"
# Avoid - generic messages
validate :email, required: true, message: "Invalid"3. Use Context-Specific Validations
class User
  # Validations that only apply during creation
  validate :password, presence: true, on: :create
  # Validations that only apply during updates
  validate :current_password, presence: true, on: :update
  # Validations that always apply
  validate :email, required: true, match: EMAIL_REGEX
end4. Test Validations Thoroughly
describe User do
  it "validates required fields" do
    user = User.new("", "", nil)
    user.valid?.should be_false
    user.errors.map(&.field).should contain(:name)
    user.errors.map(&.field).should contain(:email)
  end
  it "validates email format" do
    user = User.new("John", "invalid-email", "password")
    user.valid?.should be_false
    user.errors.map(&.message).should contain("Email format is invalid")
  end
  it "allows valid data" do
    user = User.new("John Doe", "john@example.com", "password123")
    user.valid?.should be_true
  end
endRelated Features
- Active Record Models - How to define models with validations 
- Callbacks - Lifecycle hooks that work with validations 
- CRUD Operations - How validations integrate with save operations 
- Error Handling - Working with validation errors 
Last updated
Was this helpful?
