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)
end

TypeChecker.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 TypeChecker and RateLimiter both plug into.
  • Client Methods - the RemoteFunctions whose arguments and return values TypeChecker validates.