Primitives
Primitives are the leaf validators — the ones you reach for when a field holds a single value (not an object, array, or union). Every primitive comes off the v factory.
import { v } from "@warlock.js/seal";
v.string(); // any stringv.int(); // integer (rejects 1.5)v.boolean(); // true / falsev.date(); // Date — accepts string / timestamp, normalizes to Datev.literal("a"); // type: "a"Each call returns a fresh, immutable validator. Chain methods (.email(), .min(3), .optional()) return a clone — the original is unchanged.
Strings
Section titled “Strings”v.string() // any stringv.string().email() // RFC-compliant emailv.string().url() // valid URLv.string().uuid(4) // UUID v4 specificallyv.string().min(3) // length ≥ 3v.string().max(50) // length ≤ 50v.string().regex(/^[a-z]+$/)v.email() is a shortcut for v.string().email() — switch to the full chain when you need extra rules.
v.email() // same as v.string().email()v.string().email().min(5) // when you need more than just the formatThe string surface is broad — slug normalization, mask, base64 encode/decode, HTML escape, trim variants, case conversions (.uppercase(), .camelCase(), .kebabCase()). The full method list is in the API reference.
Numbers — pick by what you accept
Section titled “Numbers — pick by what you accept”Four number validators, differing only in what they accept as input:
| Validator | Accepts | Use when |
|---|---|---|
v.number() | any finite number | accepts both integers and floats |
v.int() | integers only | rejects 1.5 |
v.float() | finite non-integers | rejects 1 |
v.numeric() | numeric strings + numbers | form/query inputs that arrive as "42" |
v.numeric() is the one that handles "42" from a query string — it coerces to a number before rules run. The others reject non-number inputs.
All four share the same chain surface — .min(0), .max(100), .between(0, 100), .positive(), .negative(), .multipleOf(5), .even(), .odd().
Booleans
Section titled “Booleans”v.boolean() // strict true / falsev.boolean().accepted() // accepts truthy form values ("on", "yes", "1", true, 1)v.boolean().declined() // opposite.accepted() / .declined() exist for form-style inputs where the wire format is a string. For JSON APIs where the client sends a real boolean, plain v.boolean() is enough.
Scalars
Section titled “Scalars”v.scalar() // string | number | booleanReach for v.scalar() only when a field genuinely accepts all three primitives. Most of the time “I want it to be flexible” is a missing discriminator — v.literal(...) or v.union([...]) reads cleaner.
v.date() // type: Date — normalizes strings/timestamps to Datev.date().past() // before nowv.date().future() // after nowv.date().min("2024-01-01") // not beforev.date().max("2024-12-31") // not afterv.date().weekDay() // Mon-Friv.date().minAge(18) // at least 18 years before todayv.date() ships a built-in mutator that parses string inputs ("2024-01-01", "2024-01-01T10:00:00Z") and timestamps to Date objects. By the time rules run, the value is a real Date. The post-validation type is Date.
For raw instanceof Date with no coercion, use v.instanceof(Date) — but v.date() is the right tool 99% of the time.
Literals
Section titled “Literals”v.literal("items") // type: "items"v.literal("draft", "published", "archived") // type: "draft" | "published" | "archived"v.literal(1, 2, 3) // type: 1 | 2 | 3v.literal(true) // type: truev.literal(...) narrows to the literal union, not the wider primitive type. Compare:
v.string().oneOf(["a", "b"]) // type: string — literal info lostv.literal("a", "b") // type: "a" | "b" — literal preservedv.enum(["a", "b"]) // type: string — array form, same as oneOfv.enum(MyTSEnum) // type: enum's value type — accepts a TS enumUse v.literal(...) for discriminator fields where the literal type matters at the call site (TypeScript narrowing inside if (x.role === "admin") blocks). Use v.enum(...) when the values come from an array or a TS enum object and the broader primitive type is fine.
Instances
Section titled “Instances”v.instanceof(File) // type: Filev.instanceof(Buffer) // type: Bufferv.instanceof(MyClass) // type: MyClassFor File uploads, Buffers, Uint8Arrays, or custom class instances. The JSON Schema output is {} (not representable) — for OpenAPI File, attach { type: "string", format: "binary" } manually after generation.
Derived values — v.computed and v.managed
Section titled “Derived values — v.computed and v.managed”These two don’t validate input — they produce a value as part of validation.
v.object({ firstName: v.string(), lastName: v.string(), fullName: v.computed<string>(({ firstName, lastName }) => `${firstName} ${lastName}` ), createdAt: v.managed<Date>(() => new Date()), createdBy: v.managed<string>(({ user }) => user.id),});v.computedruns after sibling validation; its callback receives the validateddataplus theSchemaContext. Use for derived values — full name, hash of fields, computed totals.v.managedruns fromSchemaContextonly — no sibling data. Use for framework-injected values: timestamps, current user, request id.
Both produce {} from toJsonSchema() — they’re runtime-only, not part of the JSON contract an LLM or OpenAPI consumer reads.
Escape hatch — v.any
Section titled “Escape hatch — v.any”v.any() // type: any — skips validation entirelyReach for it when you genuinely don’t care about the shape. Usually a smell — a real schema would catch a class of bugs. If you use it, leave a comment explaining why.
Quick map
Section titled “Quick map”| Need | Reach for |
|---|---|
v.string().email() or v.email() | |
| URL | v.string().url() |
| UUID | v.string().uuid() |
| Number 0–100 | v.number().between(0, 100) |
| Positive integer | v.int().positive() |
| One of N constants | v.literal(...values) |
| One of TS enum values | v.enum(MyEnum) |
| Date in the past | v.date().past() |
| File upload | v.instanceof(File) |
| Derived value | v.computed<T>(callback) |
| Framework-injected value | v.managed<T>(callback) |
| Free-form pass-through | v.any() |
Related
Section titled “Related”- Modifiers — the cross-cutting chain methods (
.optional,.default,.catch,.nullable). - Structural shapes — composing primitives into objects, arrays, unions.
- Guides → Pick the right primitive —
v.stringvsv.scalar,v.literalvsv.enum, etc.