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.
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.
| Throws | Returns | |
|---|---|---|
parse | Yes | The validated value |
safeParse | No | { 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.