Data
Guard
Runtime type checking with readable error paths. Validate remote arguments, configuration and saved data in one line.
Getting it
local Guard = Engine:GetPackage("Guard")Overview
Luau's type checker only helps while you write code. Anything that arrives at runtime, especially arguments sent by a client through a remote, can be anything at all. Guard lets you describe the shape you expect once and check values against it.
Every schema has one method:
schema:Check(value): (boolean, string?)It returns true, or false plus a message that says exactly where the value is wrong.
local Guard = Engine:GetPackage("Guard")
local BuyRequest = Guard.struct({
ItemId = Guard.string({ MaxLength = 64 }),
Amount = Guard.int({ Min = 1, Max = 99 }),
Gift = Guard.optional(Guard.int({ Min = 1 })),
}, { Strict = true })
buyRemote.OnServerEvent:Connect(function(player, request)
local ok, err = BuyRequest:Check(request)
if not ok then
warn(player.Name, "sent a bad request:", err)
-- e.g. "value.Amount: expected <= 99, got 5000"
return
end
buy(player, request.ItemId, request.Amount)
end)Strings, arrays and dictionaries have a maximum size by default (see Configuration), so a client cannot slow your server down with a huge payload.
Primitives
| Schema | Accepts |
|---|---|
Guard.string(options?) | Strings. Options: MinLength, MaxLength, Pattern (a Luau string pattern). |
Guard.number(options?) | Numbers. Options: Min, Max, AllowNaN, AllowInf (NaN and infinity are rejected unless allowed). |
Guard.int(options?) | Whole numbers. Options: Min, Max. |
Guard.boolean() | true / false. |
Guard.table() | Any table. |
Guard.func() | Functions. |
Guard.any() | Anything, including nil. |
Guard.never() | Nothing. |
Guard.range(min, max) | Shortcut for Guard.number({ Min = min, Max = max }). |
Guard.match(pattern) | Shortcut for Guard.string({ Pattern = pattern }). |
Roblox types
Guard.Vector2(), Vector3(), CFrame(), Color3(), UDim(), UDim2(), BrickColor(), NumberRange(), NumberSequence(), ColorSequence(), Rect() and Region3() check the value's typeof.
| Schema | Accepts |
|---|---|
Guard.EnumItem(enumType?) | Any EnumItem, or only items of enumType (e.g. Enum.Material). |
Guard.instance() | Any Instance. |
Guard.instanceOf(...classNames) | Instances for which :IsA() matches one of the class names. |
Combinators
| Schema | Accepts |
|---|---|
Guard.optional(schema) | nil or a value that passes schema. |
Guard.literal(value) | Exactly value. |
Guard.enum({ ...values }) | One of the listed values. |
Guard.union(...schemas) | A value that passes at least one schema. The error lists why each branch failed. |
Guard.array(element, options?) | A list where every item passes element. Options: MinLength, MaxLength. |
Guard.dict(keySchema, valueSchema, options?) | A dictionary whose keys and values pass their schemas. Option: MaxEntries. |
Guard.struct(fields, options?) | A table whose named fields pass their schemas. With Strict = true, unknown fields are rejected. |
Guard.tuple(...schemas) | A list with exactly one value per schema, in order. |
Custom checks
custom
Guard.custom(predicate: (value) -> (boolean, string?), name: string?): SchemaYour own check. Return false and optionally a message.
refine
Guard.refine(schema: Schema, predicate: (value) -> boolean, message: string?): SchemaRuns schema first, then your extra condition.
local EvenNumber = Guard.refine(Guard.int(), function(n)
return n % 2 == 0
end, "must be even")lazy
Guard.lazy(resolver: () -> Schema): SchemaDefers building a schema until it is first used, so a schema can refer to itself (trees, nested menus).
local Node
Node = Guard.struct({
Name = Guard.string(),
Children = Guard.array(Guard.lazy(function() return Node end)),
})Wrapping functions
wrap
Guard.wrap(fn: (...any) -> ...any, argSchema: Schema): (...any) -> ...anyReturns a function that checks its arguments (as an array) against argSchema before calling fn, and raises an error when they do not match. Usually combined with Guard.tuple.
local setVolume = Guard.wrap(function(bus, volume)
AudioService:SetBusVolume(bus, volume)
end, Guard.tuple(Guard.string(), Guard.range(0, 1)))Configuration
DefaultConfiguration/Packages/Guard5 settings| Setting | Default | What it does |
|---|---|---|
| DefaultMaxStringLength | 10000 | Max characters Guard.String() accepts when no length is passed. |
| DefaultMaxArrayLength | 1000 | Max entries Guard.Array() accepts when no length is passed. |
| DefaultMaxDictLength | 1000 | Max entries Guard.Dict()/Map() accept when no length is passed. |
| MaxDepth | 64 | Nesting depth a schema check will recurse into before failing. |
| MaxUnionErrorBranches | 4 | Branch errors listed in a failed Union message before truncating. |
Types
export type Schema
export type StringOptions = { MinLength: number?, MaxLength: number?, Pattern: string? }
export type NumberOptions = { Min: number?, Max: number?, AllowNaN: boolean?, AllowInf: boolean? }
export type IntOptions = { Min: number?, Max: number? }
export type ArrayOptions = { MinLength: number?, MaxLength: number? }
export type DictOptions = { MaxEntries: number? }
export type StructOptions = { Strict: boolean? }