Validation Functions
Schema-based input validation with the V class and chainable validators. Type coercion, nested schemas, password rules, and HTML passwordrules attribute generation.
Type Validators
Use the V class to create validators for each data type. Each validator can chain additional constraints.
V.string()
Validate string values. Supports length, pattern, email, URL, and character-class constraints.
Returns
Object - A chainable string validator
V.string().required().min_length(3).max_length(50)
V.string().optional().email()
V.string().nullable().url()
V.int()
Validate integer values. String inputs (e.g. "25") are automatically coerced to integers.
Returns
Object - A chainable integer validator
V.int().required().min(0).max(100)
V.int().optional().min(18)
V.int().one_of([1, 2, 3])
V.float()
Validate float values. String inputs (e.g. "95.5") are automatically coerced to floats.
Returns
Object - A chainable float validator
V.float().required().min(0.0).max(1.0)
V.float().optional().min(-100.0)
V.bool()
Validate boolean values. "true", "1", true are accepted as truthy; "false", "0", false as falsy.
Returns
Object - A chainable boolean validator
V.bool().required()
V.bool().default(false)
V.array(schema?)
Validate array values. Optionally pass an item schema to validate each element. Supports .min(n) to require at least n items.
Parameters
schema : Object? - Optional validator for each element
Returns
Object - A chainable array validator
# Array of strings
V.array(V.string().required()).required()
# Array of objects
V.array(V.hash({
"id": V.int().required(),
"name": V.string().required()
})).min(1)
V.hash(schema?)
Validate hash/object values. Optionally pass a nested schema to validate each field.
Parameters
schema : Hash? - Optional nested field validators
Returns
Object - A chainable hash validator
address_schema = V.hash({
"street": V.string().required(),
"city": V.string().required(),
"zip": V.string().pattern("^\\d{5}$")
}).required()
Chainable Methods
All validators support these methods. Call them on any V.string(), V.int(), etc.
Presence & Nullability
.required()
Field must be present in the input data
.optional()
Field may be absent (the default behaviour)
.nullable()
null is an acceptable value
.default(value)
Default value when field is missing
String Constraints
.min_length(n)
Minimum string length
.max_length(n)
Maximum string length
.pattern(regex)
Must match regex pattern (e.g. "^\\d+$")
.email()
Valid email format
.url()
Valid URL format
.letters()
Must contain at least one letter
.mixed_case()
Must contain uppercase and lowercase
.numbers()
Must contain at least one digit
.symbols()
Must contain at least one symbol character
Numeric Constraints
Enumeration
.one_of([values])
Field value must be one of the allowed values. Works with strings, ints, and any type.
V.string().required().one_of(["admin", "user", "guest"])
V.int().required().one_of([1, 2, 3])
Cross-Field Validation
.confirmation(field)
Field value must match the value of another field in the same data hash. Available on all validator types. The validation engine handles the comparison automatically during the field validation pass.
schema = {
"password": V.string().required().min_length(8),
"confirm_password": V.string().required().confirmation("password")
}
result = validate(data, schema)
# result["valid"] is false if confirm_password != password
# Error: { "field": "confirm_password", "message": "does not match", "code": "confirmation" }
Password Rules
Password character-class constraints (.letters(), .mixed_case(), .numbers(), .symbols()) serve double duty: they validate on the server side and generate the HTML passwordrules attribute that password managers (Safari, 1Password, Bitwarden) use to auto-generate compliant passwords.
.to_password_rules_string()
Serializes password-relevant constraints into the passwordrules HTML attribute format. Returns a semicolon-separated string of rules, or an empty string when no password-relevant rules are present.
password_rules = V.string()
.min_length(12)
.max_length(64)
.mixed_case()
.numbers()
.symbols()
.to_password_rules_string();
# password_rules → "minlength: 12; maxlength: 64; required: lower; required: upper; required: digit; required: special;"
# Use in a template:
# <input type="password" passwordrules="<%= password_rules %>">
validate()
validate(data, schema)
Run validation rules against a data hash. Coerces types, applies constraints, and returns a result with either the sanitized data or error details.
Parameters
data : Hash - The input data to validate (or null)schema : Hash - Schema definition with field validatorsReturns
Hash - { "valid": Bool, "data": Hash, "errors": Array }
schema = {
"name": V.string().required().min_length(2).max_length(100),
"email": V.string().required().email(),
"age": V.int().optional().min(0).max(150),
"website": V.string().optional().url()
}
result = validate({
"name": "Alice",
"email": "[email protected]",
"age": 30
}, schema)
if result["valid"]
println("Data is valid!")
println(result["data"]) # Validated & sanitized data
else
for error in result["errors"]
println(error["field"] + ": " + error["message"])
end
end
Error Format
When validation fails, result["errors"] contains an array of error objects:
{
"field": "email",
"message": "must be a valid email",
"code": "invalid_email"
}
Type Coercion
The validation system automatically coerces string values to the target type. This is especially useful for form data and JSON APIs where values arrive as strings.
schema = {
"age": V.int().required(),
"active": V.bool().required(),
"score": V.float().required()
}
# Input (strings get converted automatically)
input = {
"age": "25", # coerce to 25 (int)
"active": "true", # coerce to true (bool)
"score": "95.5" # coerce to 95.5 (float)
}
result = validate(input, schema)
# result["data"]["age"] → 25 (Int)
# result["data"]["active"] → true (Bool)
# result["data"]["score"] → 95.5 (Float)
Examples
User Registration
user_schema = {
"username": V.string().required()
.min_length(3)
.max_length(20)
.pattern("^[a-zA-Z0-9_]+$"),
"email": V.string().required().email(),
"password": V.string().required().min_length(8),
"age": V.int().optional().min(13),
"newsletter": V.bool().default(false)
}
fn register(params: Hash) -> Hash
result = validate(params, user_schema)
if !result["valid"]
return { "status": 422, "body": json_stringify({ "errors": result["errors"] }) }
end
user = User.create(result["data"])
{ "status": 201, "body": json_stringify({ "user": user }) }
end
Nested Validation
Use V.hash() and V.array() to validate complex nested structures.
order_schema = {
"customer": V.hash({
"name": V.string().required(),
"email": V.string().required().email()
}).required(),
"items": V.array(V.hash({
"product_id": V.int().required(),
"quantity": V.int().required().min(1)
})).min(1)
}
result = validate(req["json"], order_schema)
if result["valid"]
order = Order.create(result["data"])
end
Best Practices
- Validate at the boundary - Validate incoming request data in controllers before it reaches your domain logic.
- Use type coercion - Let the validator convert strings to their target types; your business logic gets properly-typed data.
- Always check result["valid"] - Never assume validation passed. Branch on
result["valid"]before usingresult["data"]. - Keep schemas DRY - Extract reusable schema fragments for fields that appear in multiple endpoints.
- Use .default() for optional fields - Provide sensible defaults so your code always has a value to work with.
- Generate password rules - When a schema has password constraints, call
.to_password_rules_string()and pass it to your template for password manager integration.