#Types
Alepha provides schema validation through the z singleton from "alepha". It wraps Zod 4 with opinionated defaults: z.text() strings carry length limits and trimming, format types are tagged for the ORM and OpenAPI, and objects strip unknown keys.
Import z from alepha, not from zod - a schema built with the raw library carries none of those defaults.
#Basic Usage
1import { z } from "alepha";2 3const userSchema = z.object({4 id: z.uuid(),5 email: z.email(),6 name: z.text(),7 age: z.integer().optional(),8});
z returns plain Zod schemas, so anything that accepts a Zod type accepts them - .parse(), .safeParse(), .optional(), and the rest of the Zod surface all work as usual.
#Strings
#z.string() vs z.text()
z.string() creates a raw string with no length limit. Use it for internal values where length is irrelevant.
z.text() adds length limits and text processing. Use it for user input, database fields, and API schemas.
1z.string(); // no limits2z.text(); // max 255 chars, trimmed3z.text({ size: "short" }); // max 64 chars, trimmed4z.text({ size: "long" }); // max 1024 chars, trimmed5z.text({ size: "rich" }); // max 65535 chars, trimmed
z.text() trims whitespace by default. You can control this and enable lowercase conversion:
1z.text({ trim: false }); // no trimming2z.text({ trim: true, lowercase: true }); // trim + lowercase
#Text Presets
Shorthand methods for common text sizes:
1z.shortText(); // same as z.text({ size: "short" }) - 64 chars2z.longText(); // same as z.text({ size: "long" }) - 1024 chars3z.richText(); // same as z.text({ size: "rich" }) - 65535 chars
#Length Limits
The size presets cap at 64 (short), 255 (regular), 1024 (long), and 65535 (rich) characters. An explicit maxLength overrides the preset cap:
1z.text({ maxLength: 1_000_000 }); // overrides the 255 default
#Numbers
1z.number(); // any number2z.integer(); // integer (no fractional part)3z.int32(); // integer clamped to signed 32-bit range (-2147483648 to 2147483647)4z.int64(); // JS-safe integer (-9007199254740991 to 9007199254740991)
z.int64() is NOT a true 64-bit integer. JavaScript cannot represent all int64 values. For true int64, use z.bigint() which stores values as strings.
Chain zod's native number checks for bounds:
1z.integer().min(0).max(100);2z.number().gt(0);
#Objects
Objects silently strip unknown keys (standard Zod behavior):
1const schema = z.object({2 name: z.text(),3 email: z.email(),4});5// { name: "alice", email: "a@b.c", extra: true } → { name: "alice", email: "a@b.c" }
Use zod's .strict() if you want extra keys to be rejected instead.
#Arrays
Arrays are unbounded by default. Add explicit bounds where the input is untrusted:
1z.array(z.string()); // no cap2z.array(z.string()).max(50); // max 50 items3z.array(z.string()).min(1); // at least 1 item
#Modifiers
#Optional and Nullable
1z.string().optional(); // string | undefined2z.string().nullable(); // string | null
These can be combined:
1z.string().nullable().optional(); // string | null | undefined
#Partial, Pick, Omit
1const user = z.object({2 id: z.uuid(),3 name: z.text(),4 email: z.email(),5});6 7user.partial(); // all fields optional8user.pick({ id: true, name: true }); // only id and name9user.omit({ id: true }); // name and email only
#Extend
Add properties to an existing schema:
1const baseUser = z.object({2 id: z.uuid(),3 name: z.text(),4});5 6const admin = baseUser.extend({7 role: z.const("admin"),8 permissions: z.array(z.text()),9});
To merge multiple base schemas, spread their .shape into a new object:
1z.object({ ...baseUser.shape, ...timestamped.shape, extra: z.text() });
#Format Types
#Bigint
String-encoded arbitrary-precision integer:
1z.bigint(); // validates "123456789", "-42", etc.
Values are represented as strings to avoid JavaScript number limitations.
#UUID
1z.uuid(); // validates UUID format (e.g. "550e8400-e29b-41d4-a716-446655440000")
#URL
1z.url(); // validates URL format
#File and Stream
1z.file(); // file-like object (browser File API compatible)2z.file({ maxBytes: 1_048_576 }); // caps what the multipart parser accepts for this route3z.stream(); // experimental streaming type4z.stream({ maxBytes: 1_048_576 }); // same cap, applied to the streamed part
maxBytes is runtime-enforced - the multipart parser reads it and refuses larger uploads
with a 413. Careful with the neighbouring $storage({ maxSize }), which is declared in
megabytes; mixing the two units up is silent in both directions.
#Domain Types
1z.email(); // validates email format (no trimming or lowercasing - whitespace is rejected)
#Phone (E.164)
1z.e164(); // validates E.164 format, e.g. "+1234567890"
#Language Tag (BCP 47)
1z.bcp47(); // validates BCP 47 tags, e.g. "en", "en-US", "fr-CA"
#Date and Time
1z.datetime(); // ISO 8601 date-time, e.g. "2026-01-15T10:30:00Z"2z.date(); // ISO 8601 date, e.g. "2026-01-15"3z.time(); // ISO 8601 time, e.g. "10:30:00"4z.duration(); // string tagged with the ISO 8601 duration format (not runtime-validated)
#Enums
String enums with built-in validation:
1z.enum(["ACTIVE", "INACTIVE", "BANNED"]);2// validates that the value is one of the listed strings
#Other Types
1z.const("value") // literal value 2z.boolean() // boolean 3z.null() // null 4z.any() // any (no validation) 5z.void() // void 6z.undefined() // undefined 7z.union([...]) // union of schemas 8z.tuple([...]) // fixed-length array 9z.record(k, v) // Record<K, V>10z.json() // Record<string, any> - convenience for JSON blobs
#Validation
Alepha validates data through alepha.codec.validate(). This is the same validation used internally by $action, $env, and other primitives.
1import { Alepha, z } from "alepha"; 2 3const alepha = Alepha.create(); 4const schema = z.object({ 5 name: z.text(), 6 email: z.email(), 7}); 8 9const result = alepha.codec.validate(schema, {10 name: " Alice ",11 email: "alice@example.com",12});13// result: { name: "Alice", email: "alice@example.com" }
Validation is a thin wrapper over schema.safeParse - everything beyond type checking lives in the schema itself:
- Trimming: strings created with
z.text()are trimmed by default (trim: falseopts out). - Lowercase: strings created with
z.text({ lowercase: true })are lowercased. - Unknown keys: objects strip keys not declared in the schema.
- Defaults:
.default(...)values are applied.
There is no extra coercion layer: null in a non-nullable field is a validation error, and a non-array value passed to an array schema is rejected, not wrapped.
If validation fails, a SchemaValidationError is thrown with details about the first failing constraint.
#Encoding
alepha.codec.encode() validates data and serializes it to a target format:
1const schema = z.object({ 2 id: z.uuid(), 3 name: z.text(), 4}); 5 6// Validate and return the cleaned object (default) 7const obj = alepha.codec.encode(schema, data); 8 9// Validate and serialize to JSON string10const json = alepha.codec.encode(schema, data, { as: "string" });11 12// Validate and serialize to binary (for protobuf, msgpack, etc.)13const bytes = alepha.codec.encode(schema, data, { as: "binary" });
You can skip validation with validation: false:
1alepha.codec.encode(schema, data, { validation: false, as: "string" });
#Codec Formats
The default codec is "json". Additional codecs like Protobuf can be registered:
1alepha.codec.register({2 name: "protobuf",3 codec: myProtobufCodec,4});5 6alepha.codec.encode(schema, data, { encoder: "protobuf", as: "binary" });
#Decoding
alepha.codec.decode() deserializes data and validates it against a schema:
1const result = alepha.codec.decode(schema, jsonString);2// result is validated and typed as Infer<typeof schema>
Specify a codec if the data isn't standard JSON:
1const result = alepha.codec.decode(schema, binaryData, { encoder: "protobuf" });
Validation runs automatically after decoding. Disable it with validation: false.
#Accessing Zod Directly
Schemas built by z are ordinary Zod schemas, so for anything z does not
wrap, the fluent API works on them directly:
1schemaA.and(schemaB);
Do not import the zod package directly for this - a second zod copy makes
schemas structurally incompatible with every Alepha primitive. Stay on the
instances z hands you.