Why I built this
Lua will happily hand you a number where you asked for a string and let it sail straight into a database mutation. Zod solved that for me in TypeScript and nothing solved it in Lua, so I built vBuilder: define a schema, parse your data, and get a clear error instead of a mystery.
What it does
vBuilder is a validation schema builder written entirely in Lua. Since Lua doesn’t have a type system or built-in validation primitives, vBuilder fills that gap by providing a chainable API to define schemas and parse incoming data against them. If the data doesn’t match, you get structured errors with the path, message, and error code.
Defining schemas
local schema = vBuilder:object({
name = vBuilder:string().min(1).max(50),
age = vBuilder:number().min(0).max(150),
role = vBuilder:enum({ "admin", "user", "moderator" }),
tags = vBuilder:array(vBuilder:string()).optional(),
})Parsing data
local result = schema.parse({
name = "John",
age = 25,
role = "admin",
})
-- Invalid data returns a ValidationError with path, message, and code
local bad = schema.parse({ name = 123 })
-- ValidationError: { path = "name", message = "Expected string", code = "invalid_type" }Supported types
- Strings: with
.min()and.max()length constraints - Numbers: with
.min()and.max()value constraints - Objects: key-value schemas with optional
.passthrough()for extra properties - Arrays: typed collections with size constraints
- Booleans: true/false validation
- Enums: case-sensitive fixed value sets
- Unions: accept multiple types for a single field
All types support .optional() to permit nil values, and custom error messages via options.
Design choices
Objects strip unrecognized properties by default, so you have to explicitly opt in with .passthrough() to keep them. This makes it safer for database mutations where you don’t want unexpected fields slipping through.