Skip to main content

Why parse and not assert

A fair question, and one worth answering properly rather than pointing at another library: parse throws when validation fails, so why not call it assert?

The short answer​

Because it hands something back, and that something is not always what you gave it.

An assertion is a guard. What comes back out of assert is what went into it, unchanged, so if it returned at all the value you already had was fine. That is the promise in the name, and naming a method assert makes the same one:

Settings:assert(raw)
-- looks safe to keep using `raw`

That promise would be false here. Parsing fills in defaults, so the value that comes back is not the value that went in:

local Settings = v.object({
Volume = v.number():min(0):max(1):default(0.5),
Muted = v.boolean():default(false),
})

local raw = { Volume = 0.2 }
local settings = Settings:parse(raw)

raw.Muted --> nil
settings.Muted --> false

Under an assert name, the line above reads as a check you can ignore the result of, and ignoring it silently drops every default. Under parse it reads as a conversion, and the shape of the call makes you keep the result. The name is doing real work.

note

The input is never modified. parse copies before writing a filled value in, and returns the original table untouched when there is nothing to fill:

local complete = { Volume = 0.2, Muted = true }
Settings:parse(complete) == complete --> true

But assert returns the value​

It does, and that is the argument for parse rather than against it.

assert(v, message) hands the value back whenever v is truthy, and Luau types it that way too — as the same value with the nil narrowed off, which is what makes this idiom work:

local part = assert(workspace:FindFirstChild("Baseplate"))
part.Name --> fine, `part` is an Instance rather than an Instance?

What comes back is that value, though, not a new one built from it:

local raw = { Volume = 0.2 }

assert(raw) == raw --> true

That is the whole difference. assert is a passthrough, so its result is spare: you can drop it, keep using the variable you already had, and lose nothing. It is why assert(cond, "...") on a line of its own is ordinary code rather than a mistake.

parse has no such guarantee. Drop its result and the defaults go with it:

local settings = Settings:parse(raw) -- Muted filled in
Settings:parse(raw) -- Muted still missing from raw

Both lines are valid Luau and nothing will warn you about the second. It is a deliberate check when you meant it as one, and a silently incomplete value when you did not — and the name is what tells a reader which to assume. assert says the result is spare. parse says the result is the point.

So the objection is right about the return and wrong about what follows from it. assert may return a value; what it may not do is return a different one. A method that does needs a name that says so.

It still asserts​

parse throws on failure, and the message names the field:

Settings:parse({ Volume = 4 })
--> Volume: expected at most 1, got 4

So if what you wanted was an assertion, you have one. Nothing about the name takes that away — it just also gives you the parsed value, and does not pretend the input was already fine.

parse and safeParse​

Two methods, one distinction: what happens when the data is wrong.

ThrowsReturns
parseYesThe validated value
safeParseNo{ success, value } or { success, issues }

Use parse when bad data is a bug — your own config, a constant, a value your code just built. Use safeParse when bad data is expected — remote arguments, DataStore reads, HTTP responses — and you want to decide what to do rather than unwind the stack:

local result = Settings:safeParse(payload)

if result.success then
apply(result.value)
else
for _, issue in result.issues do
warn(issue.path, issue.message)
end
end

On matching Roblox conventions​

The convention this usually gets measured against is t, and t is a predicate library: a checker is (value) -> (boolean, string?), and t.strict wraps one so it raises. For a predicate, assert is the right word — there is no value to hand back, so asserting is all it can do.

Vet is not a predicate library. Its checkers return the validated value, which is what makes defaults and type inference work at all. Borrowing the vocabulary of a design it does not share would be the confusing choice, not the conforming one.

Where Vet does follow the platform, it follows it properly: datatypes are checked with typeof rather than by shape, so a table carrying R, G and B is not a Color3, and the ProfileStore and DocumentService guides use each library's own conventions rather than wrapping them in something new. The name parse is not a Roblox-versus-elsewhere question — it is a question of whether the method returns something you need to keep. It does.