Skip to content

Schema

local Schema = require("@game/ReplicatedStorage/Twill").Schema
local ok, reason = Schema.Check(payload.Name, { "string", 1, 20 })
if not ok then
return refuse(reason)
end

Nothing here raises. A value that does not match is a false answer, and so is a rule that is itself malformed.

A rule is a type name, or a table whose first entry is a type name and whose later entries constrain it.

"string" -- any string
{ "string", 1, 20 } -- between 1 and 20 characters
{ "number", 0, 100 } -- between 0 and 100
{ "integer", 1 } -- whole, at least 1
{ "enum", "red", "green" } -- one of these values
{ "array", "number" } -- a real array of numbers
{ "object", { Coins = "number" } }
{ "optional", { "string", 1, 20 } }
KindConstrained by
numberA low and a high, both optional.
integerThe same, and the value must be whole.
stringA low and a high, applied to its length.
enumThe values listed after it.
arrayThe rule every element must satisfy.
objectA table of field names to rules.
optionalThe rule to apply when the value is not nil.
boolean table function thread userdata nilNothing further.

Any Roblox type name works as a kind: Vector3, Color3, Instance, EnumItem. Luau kinds are matched with type, and the rest with typeof.

NaN and infinity fail every number and integer rule, before any range is considered. Neither survives a DataStore, so a value that reached a schema on its way to storage is refused here rather than at the save.

An object rule checks the fields it lists and ignores the rest. It reads as a contract for what the caller uses, not a wall against everything else.

{ "object", { Coins = "number", Name = { "string", 1, 20 } } }

A payload carrying extra fields passes. To refuse those, check them explicitly.

Fields are checked in table order, which is not defined. A value failing two rules is reported against whichever was reached first.

An array rule counts the table’s entries and compares that count with its length. A table with a gap, or with any named key, is not an array and fails before its elements are looked at.

[Server] | [Client]

Answers whether a value satisfies a rule.

function Schema.Validate(value: any, rule: Rule): boolean

For callers that read no reason. Use Check when the reason is going somewhere.

[Server] | [Client]

Answers whether a value satisfies a rule, naming the position that failed.

function Schema.Check(value: any, rule: Rule): (boolean, string?)

Returns

boolean - false for anything the rule does not admit.

string? - Why it failed, naming the offending position.

Checking stops at the first mismatch, so the reason names one position rather than listing every problem.

export type Rule = string | { [string]: any }

The position is value at the top level, and a path below it: value.Coins, value.Items[3], value.Stats.Wins.

ReasonRaised by
value should be a numberWrong type, or a whole number rule given a fraction.
value should be a real numberNaN or infinity.
value is below the allowed minimumUnder the low bound.
value is above the allowed maximumOver the high bound.
value is too shortA string under its length bound.
value is too longA string over its length bound.
value is not one of the allowed valuesAn enum rule matching nothing.
value should be an arrayNot a table, or a table that is not a real array.
value should be a tableAn object rule given something else.
value has an invalid ruleThe rule itself is malformed, including a bound that is not a number.

Net.Handle applies a list of rules positionally to a remote’s arguments, after the wire types have already guaranteed the argument types. Use it for shapes and ranges a wire type cannot express.