Validations & Callbacks
Ensure data integrity with model validations and hook into the lifecycle of your records with callbacks.
Defining Validations
Use validates inside your model class to declare rules for each field.
Validations run automatically before create and save operations.
class User < Model
validates("email", { "presence": true, "uniqueness": true })
validates("name", { "presence": true, "min_length": 2 })
validates("age", { "numericality": true, "min": 0, "max": 150 })
validates("password", { "min_length": 8 })
end
Multiple rules per field: You can combine any number of validation options in a single validates call. All rules for the field are checked together, and every failing rule produces its own error message.
Validation Options
Each option controls a specific check on the field value:
| Option | Description |
|---|---|
presence: true |
Field must be present and not empty |
uniqueness: true |
Value must be unique in collection |
min_length: n |
String must be at least n characters |
max_length: n |
String must be at most n characters |
format: "regex" |
String must match regex pattern |
numericality: true |
Value must be a number |
min: n |
Number must be >= n |
max: n |
Number must be <= n |
custom: "method" |
Call custom validation method |
Validation Results
Operations like Model.create() return a result hash indicating success or failure.
When validation fails, the "errors" key contains an array of error objects with "field" and "message" keys.
let result = User.create({ "email": "" })
if result["valid"]
let user = result["record"]
print("Created user: " + user._key)
else
for error in result["errors"]
print(error["field"] + ": " + error["message"])
end
end
Tip: Always check result["valid"] before accessing the record. When validations fail, the record is not persisted to the database.
Custom Methods on Models
Define instance methods directly in your model class to encapsulate business logic.
Use this to access the current instance's fields.
class User < Model
def is_admin -> Bool
this.role == "admin"
end
def full_name -> String
this.first_name + " " + this.last_name
end
end
let user = User.find("user123")
if user.is_admin
print("Welcome, admin " + user.full_name())
end
Zero-arg methods: Methods like is_admin that take no arguments can be called without parentheses (e.g. user.is_admin). Methods that return a value from a computation should use parentheses when called (e.g. user.full_name()).
Callbacks
Callbacks let you hook into the lifecycle of a model record. Declare them at the class level with the name of the instance method to call.
class User < Model
before_save("normalize_email")
after_create("send_welcome_email")
before_update("log_changes")
after_delete("cleanup_related")
def normalize_email
this.email = this.email.downcase()
end
def send_welcome_email
# Send email logic
end
end
Execution order: before_save runs before both create and update operations, making it ideal for data normalization. Specific callbacks like before_create or before_update run only for their respective operation.
Available Callbacks
before_save
Before create or update
after_save
After create or update
before_create
Before inserting new record
after_create
After inserting new record
before_update
Before updating record
after_update
After updating record
before_delete
Before deleting record
after_delete
After deleting record
Complete Example
class User < Model
validates("email", { "presence": true, "uniqueness": true, "format": "^[^@]+@[^@]+$" })
validates("name", { "presence": true, "min_length": 2, "max_length": 100 })
validates("age", { "numericality": true, "min": 0, "max": 150 })
validates("password", { "min_length": 8 })
validates("role", { "custom": "validate_role" })
before_save("normalize_email")
after_create("send_welcome_email")
before_delete("cleanup_related")
def normalize_email
this.email = this.email.downcase()
end
def send_welcome_email
# Send welcome email to new users
end
def cleanup_related
# Remove associated data before deletion
end
def validate_role
let valid_roles = ["admin", "user", "moderator"]
if !valid_roles.includes?(this.role)
return "must be one of: admin, user, moderator"
end
end
def is_admin -> Bool
this.role == "admin"
end
def full_name -> String
this.first_name + " " + this.last_name
end
end
# Creating a user with validation
let result = User.create({
"email": "[email protected]",
"name": "Alice",
"age": 30,
"password": "securepass",
"role": "admin"
})
if result["valid"]
let user = result["record"]
print(user.email) # => "[email protected]" (normalized by before_save)
print(user.is_admin) # => true
print(user.full_name()) # => "Alice "
end
Best Practices
- Validate early - Add validations to catch bad data before it reaches the database
- Use presence checks - Required fields should always have
presence: true - Keep callbacks focused - Each callback method should do one thing well
- Avoid heavy logic in callbacks - Don't put slow operations in
before_saveorbefore_create - Use custom validations - For complex rules that can't be expressed with built-in options
- Check validation results - Always inspect
result["valid"]aftercreate