TypeChecker
TypeChecker validates a remote's incoming arguments (Inbound) and return values (Outbound) used as Middleware, same as RateLimiter. It closes off an entire class of bugs where a malicious or buggy client sends malformed data straight into your Service.
Supported schema syntax
| Syntax | Example | Meaning |
|---|---|---|
| Simple type | "string" |
non-nil string |
| Optional | "number?" |
number or nil |
| Union | "string|number" |
string or number |
| Optional union | "Part|Model?" |
Part, Model, or nil |
| Roblox instance | "BasePart" |
inherits from BasePart (IsA) |
| Roblox type | "Vector3" |
typeof(value) == "Vector3" |
| Structured table | { x = "number", y = "number?" } |
table with typed fields |
| Homogeneous array | { __array = "string" } |
table where every value is a string |
| Nested | { pos = { x = "number" } } |
nested tables, any depth |
| Literal | true, 42, "hello" |
strict equality |
| Any | "any" |
accepts anything |
TypeChecker.args(...)
Validates arguments coming from the client before your method is ever called:
local TypeChecker = Owl.Util.TypeChecker
local TradeService = Owl.CreateService({
Name = "TradeService",
Middleware = {
Inbound = {
-- > // Arg 1: string, Arg 2: number (not nil)
TypeChecker.args("string", "number"),
},
},
Client = {
RequestTrade = function(self, plr, targetName, amount)
-- > // Guaranteed here: targetName is a string, amount is a number
end,
},
})With unions and optional types:
TypeChecker.args(
"string|number", -- > // arg 1: string OR number
"BasePart?", -- > // arg 2: BasePart (or subclass) OR nil
"boolean" -- > // arg 3: required boolean
)With structured tables:
TypeChecker.args(
"string",
{ -- > // arg 2: table with typed fields
x = "number",
y = "number",
label = "string?", -- > // optional
}
)With arrays:
TypeChecker.args(
{ __array = "string" }, -- > // arg 1: list of strings
{ __array = "number?" } -- > // arg 2: list of numbers or nils
)TypeChecker.returns(...)
Validates what the server returns, before it's sent to the client useful for catching server-side bugs (an unexpected nil, a wrong type) before they ever reach the client:
local DataService = Owl.CreateService({
Name = "DataService",
Middleware = {
Outbound = {
-- > // Guarantees the return value is always a number
TypeChecker.returns("number"),
},
},
Client = {
GetCoins = function(self, plr)
return self._data[plr.UserId] -- > // if nil, Outbound warns before sending
end,
},
})TypeChecker.validate(value, schema)
Validates a value directly in your own code, without going through middleware handy for validating internal data:
local ok, err = TypeChecker.validate(someValue, "number")
-- > // ok = true/false, err = error message or nil
local ok, err = TypeChecker.validate(data, {
userId = "number",
name = "string",
coins = "number?",
})
local ok, err = TypeChecker.validate(tags, { __array = "string" })
if not ok then
warn("Invalid data:", err)
endTypeChecker.compile(schema)
Compiles a schema once and returns a reusable function worth it any time the same validation runs often, to avoid recompiling the schema on every call:
-- > // Compiled once, at module load
local checkPosition = TypeChecker.compile({ x = "number", y = "number", z = "number?" })
local checkInventory = TypeChecker.compile({ __array = "string" })
-- > // Reused everywhere, no recompilation
local ok, err = checkPosition({ x = 1, y = 2 }) -- > // true
local ok, err = checkPosition({ x = 1 }) -- > // false, ".y: Expected number, got nil"
local ok, err = checkInventory({ "Sword", "Shield" }) -- > // true
local ok, err = checkInventory({ "Sword", 42 }) -- > // false, "[2]: Expected string, got number"Combining RateLimiter + TypeChecker
In practice, you stack both for full protection:
local RateLimiter = Owl.Util.RateLimiter
local TypeChecker = Owl.Util.TypeChecker
local ShopService = Owl.CreateService({
Name = "ShopService",
Middleware = {
Inbound = {
-- 1. Rate limit first (rejects fast, without validating types)
RateLimiter.strict(3, 1, "ShopService"),
-- 2. Then type validation
TypeChecker.args("string", "number"),
},
},
Client = { --[[ ... ]] },
})The order matters: rejecting a spammed request via RateLimiter is cheap, so it runs first there's no reason to pay the cost of type-checking arguments on a request that's about to be thrown away anyway.
Where to go next
- Middleware - the cascade
TypeCheckerandRateLimiterboth plug into. - Client Methods - the RemoteFunctions whose arguments and return values
TypeCheckervalidates.