Validation
Validation
Runtime validation at the edge. In Node mode any Standard Schema library works. npm schema libraries do not compile into a ch3 ELF, so the repo ships @ch3/val, a subset-native Standard Schema library.
TL;DR
| Mode | Validation | Throughput (POST /order, -c64 -d6 x3, median) |
|---|---|---|
Node (node apps/*.ts) |
none (read body only) | ~72k req/s |
| Node | JSON.parse only |
~65k req/s |
| Node | @ch3/val safeParse |
~57.9k req/s (same band as valibot) |
| ELF | @ch3/val safeParse |
~73k req/s (-26% vs unvalidated ELF) |
ELF (scriptc build) |
none | ~97-104k req/s |
- In Node mode validation costs ~10-18% of request throughput, comparable to what
JSON.parseitself costs. Pick any Standard Schema library on API taste. - In ELF mode
@ch3/valis the way to validate inside the compiled binary.
@ch3/val
packages/val is a minimal Standard Schema v1 library written inside the same compilable subset as ch3. scriptc coverage on a ch3 app using it: 100% static.
API mirrors valibot where the subset allows:
import * as v from "@ch3/val";
const Item = v.strictObject({
sku: v.pipe(v.string(), v.minLength(1)),
qty: v.pipe(v.number(), v.integer(), v.minValue(1)),
price: v.pipe(v.number(), v.finite()),
});
const Order = v.strictObject({
id: v.pipe(v.string(), v.uuid()),
email: v.pipe(v.string(), v.email()),
items: v.pipe(v.array(Item), v.minLength(1)),
note: v.optional(v.pipe(v.string(), v.maxLength(200))),
express: v.boolean(),
coupon: v.nullable(v.string()),
});
const r = v.safeParse(Order, JSON.parse(c.req.body()));
// r = { success, output, issues } — issues carry type/expected/received/
// message/path, paths are plain key arrays: ["items", "1", "qty"]
Shipped surface: string number boolean any unknown never literal picklist nullable optional(+Str/Num/Bool defaults) nullish array tuple object looseObject strictObject record union custom pipe, actions minLength maxLength length nonEmpty empty regex email uuid url ipv4 digits startsWith endsWith includes integer finite safeInteger minValue maxValue gtValue ltValue multipleOf check transform trim trimStart trimEnd toLowerCase toUpperCase toMinValue toMaxValue, entry points parse safeParse is assert flatten summarize VError InferOutput, plus spec-conformant schema["~standard"].
Deliberate divergences
- Results are plain records (
{ success, output, issues }), not classes. VIssuehas noinputmember.literal()andoptional()defaults accept string/number/boolean/null.optionalwith a default isoptionalStr/optionalNum/optionalBool.InferOutputresolves concretely forstring/number/booleanschemas; objects/arrays/wrappers resolve to open record types.- No
Date/File/Map/Setschemas, no async schemas, noenum_object (usepicklist).
How it stays fast
Kind-tagged schema classes (same pattern as ch3’s Context/ReqView) with a module-level VFail sentinel for failure identity, and a shared push/pop path stack: no exception or record allocation on the happy path beyond the output record. Validation costs ~26% of ELF request throughput on the bench schema.
Recommendation
- Node builds: any Standard Schema library; validation is not the bottleneck.
- ELF builds:
@ch3/val.