ESC
Type to search...
S
Soli Docs

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

.min(n)

Minimum value for ints/floats. Minimum length for arrays.

.max(n)

Maximum value for ints/floats

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 validators

Returns

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 using result["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.