Validation
Azu provides comprehensive input validation using the Schema library, offering type-safe validation with detailed error messages, custom validation rules, and seamless integration with request contracts.
What is Validation?
Validation in Azu provides:
Type Safety: Compile-time type checking for validation rules
Comprehensive Rules: Built-in validation rules for common scenarios
Custom Validation: Support for custom validation logic
Error Messages: Detailed, actionable error messages
Integration: Seamless integration with request contracts
Basic Validation
Simple Validation
struct UserRequest
include Azu::Request
getter name : String
getter email : String
getter age : Int32?
def initialize(@name = "", @email = "", @age = nil)
end
# Basic validation rules
validate name, presence: true
validate email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validate age, numericality: {greater_than: 0, less_than: 150}, allow_nil: true
endValidation Methods
# Check if request is valid
if user_request.valid?
# Process valid request
process_user(user_request)
else
# Handle validation errors
handle_errors(user_request.errors)
end
# Force validation (raises exception if invalid)
begin
user_request.validate!
rescue ValidationError => e
# Handle validation error
end
# Get validation errors
errors = user_request.errors
error_messages = user_request.errors.map(&.message)Built-in Validation Rules
Presence Validation
struct PresenceRequest
include Azu::Request
getter name : String
getter email : String
getter phone : String?
def initialize(@name = "", @email = "", @phone = nil)
end
# Required fields
validate name, presence: true
validate email, presence: true
# Optional fields (no presence validation)
# phone is optional
endLength Validation
struct LengthRequest
include Azu::Request
getter title : String
getter description : String
getter password : String
def initialize(@title = "", @description = "", @password = "")
end
# Exact length
validate title, length: {exactly: 50}
# Minimum length
validate password, length: {min: 8}
# Maximum length
validate description, length: {max: 500}
# Range
validate title, length: {min: 5, max: 100}
endFormat Validation
struct FormatRequest
include Azu::Request
getter email : String
getter phone : String
getter url : String
getter username : String
def initialize(@email = "", @phone = "", @url = "", @username = "")
end
# Email format
validate email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
# Phone format
validate phone, format: /\A\+?[\d\s\-\(\)]+\z/
# URL format
validate url, format: /\Ahttps?:\/\/.+\z/
# Username format (alphanumeric and underscores)
validate username, format: /\A[a-zA-Z0-9_]+\z/
endNumerical Validation
struct NumericalRequest
include Azu::Request
getter age : Int32
getter price : Float64
getter score : Int32?
getter percentage : Float64?
def initialize(@age = 0, @price = 0.0, @score = nil, @percentage = nil)
end
# Greater than
validate age, numericality: {greater_than: 0}
# Less than
validate age, numericality: {less_than: 150}
# Range
validate age, numericality: {greater_than: 0, less_than: 150}
# Allow nil
validate score, numericality: {greater_than: 0, less_than: 100}, allow_nil: true
# Float validation
validate price, numericality: {greater_than: 0.0}
validate percentage, numericality: {greater_than: 0.0, less_than: 100.0}, allow_nil: true
endInclusion/Exclusion Validation
struct InclusionRequest
include Azu::Request
getter status : String
getter priority : String?
getter role : String
def initialize(@status = "", @priority = nil, @role = "")
end
# Must be one of the specified values
validate status, inclusion: {in: ["active", "inactive", "pending"]}
# Optional inclusion
validate priority, inclusion: {in: ["low", "medium", "high"]}, allow_nil: true
# Must not be one of the specified values
validate role, exclusion: {in: ["admin", "root", "system"]}
endCustom Validation
Custom Validation Methods
struct CustomValidationRequest
include Azu::Request
getter password : String
getter confirm_password : String
getter username : String
getter email : String
def initialize(@password = "", @confirm_password = "", @username = "", @email = "")
end
# Custom validation methods
validate password, custom: :validate_password_strength
validate confirm_password, custom: :validate_password_match
validate username, custom: :validate_username_availability
validate email, custom: :validate_email_availability
private def validate_password_strength
return if @password.empty?
# Check minimum length
if @password.size < 8
errors.add("password", "must be at least 8 characters long")
end
# Check for uppercase letter
unless @password.match(/[A-Z]/)
errors.add("password", "must contain at least one uppercase letter")
end
# Check for lowercase letter
unless @password.match(/[a-z]/)
errors.add("password", "must contain at least one lowercase letter")
end
# Check for number
unless @password.match(/\d/)
errors.add("password", "must contain at least one number")
end
# Check for special character
unless @password.match(/[!@#$%^&*(),.?":{}|<>]/)
errors.add("password", "must contain at least one special character")
end
end
private def validate_password_match
return if @confirm_password.empty?
if @password != @confirm_password
errors.add("confirm_password", "must match password")
end
end
private def validate_username_availability
return if @username.empty?
# Check if username is available
if User.exists?(username: @username)
errors.add("username", "is already taken")
end
# Check for reserved usernames
reserved_usernames = ["admin", "root", "system", "api", "www"]
if reserved_usernames.includes?(@username.downcase)
errors.add("username", "is reserved")
end
end
private def validate_email_availability
return if @email.empty?
# Check if email is available
if User.exists?(email: @email)
errors.add("email", "is already taken")
end
end
endConditional Validation
struct ConditionalValidationRequest
include Azu::Request
getter user_type : String
getter company_name : String?
getter personal_email : String?
getter business_email : String?
def initialize(@user_type = "", @company_name = nil, @personal_email = nil, @business_email = nil)
end
validate user_type, inclusion: {in: ["individual", "business"]}
# Company name required for business users
validate company_name, presence: true, if: :business_user?
# Personal email required for individual users
validate personal_email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, if: :individual_user?
# Business email required for business users
validate business_email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, if: :business_user?
private def business_user?
@user_type == "business"
end
private def individual_user?
@user_type == "individual"
end
endError Handling
Validation Error Response
struct ValidationErrorResponse
include Azu::Response
def initialize(@errors : Hash(String, Array(String)))
end
def render
{
"Status" => "Unprocessable Entity",
"Title" => "Validation Error",
"Detail" => "The request could not be processed due to validation errors.",
"FieldErrors" => @errors,
"Timestamp" => Time.utc.to_rfc3339
}.to_json
end
endError Handling in Endpoints
struct CreateUserEndpoint
include Azu::Endpoint(CreateUserRequest, UserResponse)
post "/users"
def call : UserResponse
# Validate request
unless create_user_request.valid?
raise Azu::Response::ValidationError.new(
create_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
)
end
# Process valid request
user = create_user(create_user_request)
UserResponse.new(user)
end
endAdvanced Validation
Nested Object Validation
struct Address
include Azu::Request
getter street : String
getter city : String
getter state : String
getter zip_code : String
def initialize(@street = "", @city = "", @state = "", @zip_code = "")
end
validate street, presence: true
validate city, presence: true
validate state, presence: true
validate zip_code, format: /\A\d{5}(-\d{4})?\z/
end
struct UserWithAddressRequest
include Azu::Request
getter name : String
getter email : String
getter address : Address
def initialize(@name = "", @email = "", @address = Address.new)
end
validate name, presence: true
validate email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validate address, presence: true
endArray Validation
struct Tag
include Azu::Request
getter name : String
getter color : String
def initialize(@name = "", @color = "")
end
validate name, presence: true, length: {min: 1, max: 20}
validate color, inclusion: {in: ["red", "blue", "green", "yellow", "purple"]}
end
struct PostWithTagsRequest
include Azu::Request
getter title : String
getter content : String
getter tags : Array(Tag)
def initialize(@title = "", @content = "", @tags = [] of Tag)
end
validate title, presence: true, length: {min: 5, max: 100}
validate content, presence: true, length: {min: 10}
validate tags, length: {min: 1, max: 5}, message: "Must have between 1 and 5 tags"
endFile Validation
struct FileUploadRequest
include Azu::Request
getter file : HTTP::FormData::File
getter description : String?
def initialize(@file = HTTP::FormData::File.new("", "", "", 0), @description = nil)
end
# File type validation
validate file, file_type: ["image/jpeg", "image/png", "image/gif", "application/pdf"]
# File size validation
validate file, file_size: {max: 10.megabytes}
# Custom file validation
validate file, custom: :validate_file_content
validate description, length: {max: 500}, allow_nil: true
private def validate_file_content
return if @file.content.empty?
# Check file signature
case @file.content_type
when "image/jpeg"
unless @file.content.starts_with?([0xFF, 0xD8, 0xFF])
errors.add("file", "Invalid JPEG file")
end
when "image/png"
unless @file.content.starts_with?([0x89, 0x50, 0x4E, 0x47])
errors.add("file", "Invalid PNG file")
end
when "application/pdf"
unless @file.content.starts_with?("%PDF")
errors.add("file", "Invalid PDF file")
end
end
end
endValidation Testing
Unit Testing
require "spec"
describe UserRequest do
it "validates required fields" do
request = UserRequest.new(name: "", email: "")
request.valid?.should be_false
request.errors.any? { |e| e.field == "name" }.should be_true
request.errors.any? { |e| e.field == "email" }.should be_true
end
it "validates email format" do
request = UserRequest.new(name: "Alice", email: "invalid-email")
request.valid?.should be_false
request.errors.any? { |e| e.field == "email" }.should be_true
end
it "accepts valid data" do
request = UserRequest.new(
name: "Alice",
email: "alice@example.com",
age: 30
)
request.valid?.should be_true
end
endIntegration Testing
describe "Validation Integration" do
it "handles validation errors in endpoints" do
request = CreateUserRequest.new(name: "", email: "invalid")
endpoint = CreateUserEndpoint.new
expect_raises(Azu::Response::ValidationError) do
endpoint.call
end
end
it "processes valid requests" do
request = CreateUserRequest.new(
name: "Alice",
email: "alice@example.com",
age: 30
)
endpoint = CreateUserEndpoint.new
response = endpoint.call
response.should be_a(UserResponse)
end
endPerformance Considerations
Lazy Validation
class LazyValidationRequest
include Azu::Request
getter name : String
getter email : String
def initialize(@name = "", @email = "")
end
validate name, presence: true
validate email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
# Lazy validation for expensive operations
validate email, custom: :validate_email_availability, if: :email_present?
private def email_present?
!@email.empty?
end
private def validate_email_availability
# Only validate if email is present
return if @email.empty?
# Expensive database check
if User.exists?(email: @email)
errors.add("email", "is already taken")
end
end
endValidation Caching
class CachedValidationRequest
include Azu::Request
getter username : String
def initialize(@username = "")
end
validate username, presence: true, custom: :validate_username_availability
private def validate_username_availability
return if @username.empty?
# Check cache first
cache_key = "username_available:#{@username}"
if cached = Azu.cache.get(cache_key)
unless cached == "true"
errors.add("username", "is already taken")
end
return
end
# Check database
available = !User.exists?(username: @username)
# Cache result
Azu.cache.set(cache_key, available.to_s, ttl: 1.hour)
unless available
errors.add("username", "is already taken")
end
end
endBest Practices
1. Use Appropriate Validation Rules
# Good: Appropriate validation rules
validate email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validate age, numericality: {greater_than: 0, less_than: 150}
validate password, length: {min: 8}
# Avoid: Overly restrictive rules
validate email, format: /\A[a-z]+@[a-z]+\.[a-z]+\z/ # Too restrictive
validate age, numericality: {greater_than: 18, less_than: 65} # Too restrictive2. Provide Clear Error Messages
# Good: Clear error messages
validate name, presence: true, message: "Name is required"
validate email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
message: "Please enter a valid email address"
validate age, numericality: {greater_than: 0, less_than: 150},
message: "Age must be between 1 and 149"
# Avoid: Generic error messages
validate name, presence: true # Generic message
validate email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i # Generic message3. Use Conditional Validation
# Good: Conditional validation
validate company_name, presence: true, if: :business_user?
validate personal_email, presence: true, if: :individual_user?
# Avoid: Always validating optional fields
validate company_name, presence: true # Always required
validate personal_email, presence: true # Always required4. Handle Validation Errors Gracefully
# Good: Handle errors gracefully
def call : UserResponse
unless request.valid?
raise Azu::Response::ValidationError.new(
request.errors.group_by(&.field).transform_values(&.map(&.message))
)
end
# Process valid request
end
# Avoid: Ignoring validation errors
def call : UserResponse
# No validation check
process_request
end5. Test Validation Thoroughly
# Good: Test all validation scenarios
describe "Validation" do
it "validates required fields" do
# Test missing required fields
end
it "validates field formats" do
# Test invalid formats
end
it "validates field lengths" do
# Test length constraints
end
it "validates custom rules" do
# Test custom validation
end
endNext Steps
Now that you understand validation:
Request Contracts - Use validation in request contracts
Error Handling - Handle validation errors
Testing - Test validation rules
Security - Implement security validation
Performance - Optimize validation performance
Validation in Azu provides a powerful way to ensure data integrity and security. With comprehensive rules, custom validation, and detailed error messages, it makes building robust applications straightforward and reliable.
Last updated
Was this helpful?
