Skip to content

Attributes - Validations

Ensure inputs meet requirements before execution. Validations run after coercions, giving you declarative data integrity checks.

See Global Configuration for custom validator setup.

Usage

Define validation rules on attributes to enforce data requirements:

class ProcessSubscription < CMDx::Task
  # Required field with presence validation
  attribute :user_id, presence: true

  # String with length constraints
  attribute :preferences, length: { minimum: 10, maximum: 500 }

  # Numeric range validation
  attribute :tier_level, inclusion: { in: 1..5 }

  # Format validation for email
  attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

  def work
    user_id       #=> "98765"
    preferences   #=> "Send weekly digest emails"
    tier_level    #=> 3
    contact_email #=> "user@company.com"
  end
end

ProcessSubscription.execute(
  user_id: "98765",
  preferences: "Send weekly digest emails",
  tier_level: 3,
  contact_email: "user@company.com"
)

Tip

Validations run after coercions, so you can validate the final coerced values rather than raw input.

Built-in Validators

Common Options

This list of options is available to all validators:

Option Description
:allow_nil Skip validation when value is nil
:if Symbol, proc, lambda, or callable determining when to validate
:unless Symbol, proc, lambda, or callable determining when to skip validation
:message Custom error message for validation failures

Exclusion

class ProcessProduct < CMDx::Task
  attribute :status, exclusion: { in: %w[recalled archived] }

  def work
    # Your logic here...
  end
end
Options Description
:in The collection of forbidden values or range
:within Alias for :in option
:of_message Custom message for discrete value exclusions
:in_message Custom message for range-based exclusions
:within_message Alias for :in_message option

Format

class ProcessProduct < CMDx::Task
  attribute :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/

  attribute :sku, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ }

  def work
    # Your logic here...
  end
end
Options Description
regexp Alias for :with option
:with Regex pattern that the value must match
:without Regex pattern that the value must not match

Inclusion

class ProcessProduct < CMDx::Task
  attribute :availability, inclusion: { in: %w[available limited] }

  def work
    # Your logic here...
  end
end
Options Description
:in The collection of allowed values or range
:within Alias for :in option
:of_message Custom message for discrete value inclusions
:in_message Custom message for range-based inclusions
:within_message Alias for :in_message option

Length

class CreateBlogPost < CMDx::Task
  attribute :title, length: { within: 5..100 }

  def work
    # Your logic here...
  end
end
Options Description
:within Range that the length must fall within (inclusive)
:not_within Range that the length must not fall within
:in Alias for :within
:not_in Range that the length must not fall within
:min Minimum allowed length
:max Maximum allowed length
:is Exact required length
:is_not Length that is not allowed
:within_message Custom message for within/range validations
:in_message Custom message for :in validation
:not_within_message Custom message for not_within validation
:not_in_message Custom message for not_in validation
:min_message Custom message for minimum length validation
:max_message Custom message for maximum length validation
:is_message Custom message for exact length validation
:is_not_message Custom message for is_not validation

Numeric

class CreateBlogPost < CMDx::Task
  attribute :word_count, numeric: { min: 100 }

  def work
    # Your logic here...
  end
end
Options Description
:within Range that the value must fall within (inclusive)
:not_within Range that the value must not fall within
:in Alias for :within option
:not_in Alias for :not_within option
:min Minimum allowed value (inclusive, >=)
:max Maximum allowed value (inclusive, <=)
:is Exact value that must match
:is_not Value that must not match
:within_message Custom message for range validations
:not_within_message Custom message for exclusion validations
:min_message Custom message for minimum validation
:max_message Custom message for maximum validation
:is_message Custom message for exact match validation
:is_not_message Custom message for exclusion validation

Presence

class CreateBlogPost < CMDx::Task
  attribute :content, presence: true

  attribute :content, presence: { message: "cannot be blank" }

  def work
    # Your logic here...
  end
end
Options Description
true Ensures value is not nil, empty string, or whitespace

Declarations

Important

Custom validators must raise CMDx::ValidationError with a descriptive message.

Proc or Lambda

Use anonymous functions for simple validation logic:

class SetupApplication < CMDx::Task
  # Proc
  register :validator, :api_key, proc do |value, options = {}|
    unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
      raise CMDx::ValidationError, "invalid API key format"
    end
  end

  # Lambda
  register :validator, :api_key, ->(value, options = {}) {
    unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
      raise CMDx::ValidationError, "invalid API key format"
    end
  }
end

Class or Module

Register custom validation logic for specialized requirements:

class ApiKeyValidator
  def self.call(value, options = {})
    unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
      raise CMDx::ValidationError, "invalid API key format"
    end
  end
end

class SetupApplication < CMDx::Task
  register :validator, :api_key, ApiKeyValidator

  attribute :access_key, api_key: true
end

Removals

Remove unwanted validators:

Warning

Each deregister call removes one validator. Use multiple calls for batch removals.

class SetupApplication < CMDx::Task
  deregister :validator, :api_key
end

Error Handling

Validation failures provide detailed, structured error messages:

class CreateProject < CMDx::Task
  attribute :project_name, presence: true, length: { minimum: 3, maximum: 50 }
  attribute :budget, numeric: { greater_than: 1000, less_than: 1000000 }
  attribute :priority, inclusion: { in: [:low, :medium, :high] }
  attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

  def work
    # Your logic here...
  end
end

result = CreateProject.execute(
  project_name: "AB",           # Too short
  budget: 500,                  # Too low
  priority: :urgent,            # Not in allowed list
  contact_email: "invalid-email"    # Invalid format
)

result.state    #=> "interrupted"
result.status   #=> "failed"
result.reason   #=> "Invalid"
result.metadata #=> {
                #     errors: {
                #       full_message: "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid.",
                #       messages: {
                #         project_name: ["is too short (minimum is 3 characters)"],
                #         budget: ["must be greater than 1000"],
                #         priority: ["is not included in the list"],
                #         contact_email: ["is invalid"]
                #       }
                #     }
                #   }