Odel

Validators

Zod schemas for the input types modules keep needing.

validators is a set of Zod schemas for things every other module also validates. They are ordinary Zod, so they compose with .optional(), .default(), and everything else you would normally reach for.

import { validators } from '@odel/module-sdk';

inputSchema: {
	to: validators.emailList(),
	subject: validators.boundedString(1, 200),
	replyTo: validators.email().optional()
}

Text

ValidatorAccepts
nonEmptyString()At least one character — including whitespace. " " passes.
trimmedString()Trims first, then requires a character. " hi " becomes "hi"; " " fails.
boundedString(min, max)Length between min and max
optionalString()Optional, and turns "" into undefined

nonEmptyString and trimmedString are easy to mix up. For anything a human typed into a form, trimmedString is almost always the one you want — a subject line of three spaces is not a subject line.

Email

validators.email();     // one address
validators.emailList(); // an array, or a comma-separated string

emailList accepts ["a@b.com", "c@d.com"] or "a@b.com, c@d.com" and always gives you an array of validated addresses. Surrounding whitespace is trimmed, so a list pasted out of a mail client works.

URLs

validators.url();      // any URL Zod accepts
validators.httpsUrl(); // must start with https://

url() is broader than it looks — it accepts ftp://, mailto:, and javascript: too. If your module is going to fetch the value, or put it in an href, use httpsUrl().

Numbers

ValidatorAccepts
positiveInt()Integer greater than 0
nonNegativeInt()Integer 0 or greater
port()Integer 1–65535

Keys and identifiers

validators.apiKey();      // at least 10 characters
validators.apiKey('sk-'); // …and must start with "sk-"
validators.uuid();        // any UUID version, not just v4

The prefix form catches the most common support ticket a module gets: a key pasted from the wrong service. Failing at the schema with API key must start with "sk-" is a better outcome than a 401 from upstream three calls later.

Dates and enums

validators.isoDate(); // ISO 8601 *datetime* — "2024-01-15T10:30:00Z"
validators.enumFrom(['draft', 'published', 'archived'] as const);

isoDate requires a time component. A bare "2024-01-15" fails, so if you accept plain dates, use your own z.string().regex(...).

enumFrom needs as const to infer the literal union — without it the array widens to string[] and you lose the type.

JSON

validators.json<{ enabled: boolean }>();
// '{"enabled":true}'  ->  { enabled: true }

This parses the string and casts to T. It does not check that the parsed value matches the type — invalid JSON fails, but '{"enabled":"yes"}' passes and hands you a lie. When the shape matters, parse and then validate:

validators.json().pipe(z.object({ enabled: z.boolean() }));

Nothing here is required

These exist to save typing, not to gate you. A tool that needs something specific should use plain Zod, and mixing the two in one schema is normal:

import { z } from 'zod';

inputSchema: {
	to: validators.emailList(),
	body: z.string(),
	priority: z.enum(['low', 'normal', 'high']).default('normal')
}

On this page