Skip to content
Engine

Data

Guard

Runtime type checking with readable error paths. Validate remote arguments, configuration and saved data in one line.

PackageShared

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.

Validating a remote
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

SchemaAccepts
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.

SchemaAccepts
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

SchemaAccepts
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?): Schema

Your own check. Return false and optionally a message.

refine

Guard.refine(schema: Schema, predicate: (value) -> boolean, message: string?): Schema

Runs 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): Schema

Defers 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) -> ...any

Returns 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

Defaults from DefaultConfiguration/Packages/Guard5 settings
SettingDefaultWhat it does
DefaultMaxStringLength10000Max characters Guard.String() accepts when no length is passed.
DefaultMaxArrayLength1000Max entries Guard.Array() accepts when no length is passed.
DefaultMaxDictLength1000Max entries Guard.Dict()/Map() accept when no length is passed.
MaxDepth64Nesting depth a schema check will recurse into before failing.
MaxUnionErrorBranches4Branch 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? }