Use this page when you maintain code that already uses @agentuity/schema. New v3 examples should generally use Zod, ArkType, or Valibot because Agentuity accepts Standard Schema-compatible validators.
@agentuity/schema remains available for existing projects, but new v3 docs and starters should prefer Zod, ArkType, or Valibot. Keep using this reference when you are migrating older code or need the package-specific helpers below.
StandardSchema Support
@agentuity/schema implements the StandardSchemaV1 interface. That makes schemas usable at app boundaries, service helpers, AI SDK structured output calls, and any code that accepts StandardSchema validators.
Type Inference
Derive TypeScript types from the schema instead of writing a second interface.
import { s } from '@agentuity/schema';
const SearchInputSchema = s.object({
query: s.string().min(1),
category: s.enum(['docs', 'issues', 'pull-requests']),
limit: s.optional(s.number().min(1).max(20)),
});
const SearchResultSchema = s.object({
results: s.array(
s.object({
id: s.string(),
title: s.string(),
score: s.number(),
})
),
total: s.number(),
});
type SearchInput = s.infer<typeof SearchInputSchema>;
type SearchResult = s.infer<typeof SearchResultSchema>;
async function searchDocuments(input: SearchInput): Promise<SearchResult> {
const limit = input.limit ?? 10;
return {
results: [{ id: 'doc_123', title: `Result for ${input.query}`, score: 0.95 }],
total: limit,
};
}
export async function POST(request: Request): Promise<Response> {
const input = SearchInputSchema.parse(await request.json());
const output = await searchDocuments(input);
return Response.json(SearchResultSchema.parse(output));
}Type inference keeps route code, shared functions, and tests tied to the same source of truth. Change the schema and callers see the new type.
Primitive Types
The s builder provides schemas for all JavaScript primitive types.
| Schema | Validates | Example |
|---|---|---|
s.string() | string values | s.string().parse('hello') |
s.number() | number values | s.number().parse(42) |
s.boolean() | boolean values | s.boolean().parse(true) |
s.null() | null only | s.null().parse(null) |
s.undefined() | undefined only | s.undefined().parse(undefined) |
s.unknown() | any value, typed as unknown | s.unknown().parse('anything') |
s.any() | any value, typed as any | s.any().parse(123) |
s.string() supports chainable refinements for common validations:
import { s } from '@agentuity/schema';
const email = s.string().email();
email.parse('user@example.com'); // 'user@example.com'
const username = s.string().min(3).max(20);
username.parse('alice'); // 'alice'
const website = s.string().url();
website.parse('https://agentuity.dev'); // 'https://agentuity.dev'Every schema supports .describe() for documentation. Descriptions carry through to JSON Schema output:
const age = s.number().describe('Age in years');Complex Types
Build structured schemas with s.object(), s.array(), and s.record().
import { s } from '@agentuity/schema';
const UserSchema = s.object({
name: s.string(),
age: s.number(),
tags: s.array(s.string()),
});
type User = s.infer<typeof UserSchema>;
// { name: string; age: number; tags: string[] }
const user = UserSchema.parse({
name: 'Alice',
age: 30,
tags: ['admin', 'active'],
});Object schemas support .pick(), .omit(), .partial(), and .extend() for deriving new schemas:
const CreateUser = UserSchema.omit(['age']);
// { name: string; tags: string[] }
const UpdateUser = UserSchema.partial();
// { name?: string; age?: number; tags?: string[] }
const AdminUser = UserSchema.extend({
role: s.literal('admin'),
permissions: s.array(s.string()),
});
// { name: string; age: number; tags: string[]; role: 'admin'; permissions: string[] }s.record() validates objects with dynamic keys, like TypeScript's Record<string, T>:
const config = s.record(s.string(), s.number());
config.parse({ timeout: 30, retries: 3 }); // OK
config.parse({ timeout: 'fast' }); // throws ValidationErrorUtility Types
Utility schemas modify or combine other schemas.
| Schema | Effect | Output Type |
|---|---|---|
s.literal(value) | Exact value match | value (literal type) |
s.optional(schema) | Allows undefined | T | undefined |
s.nullable(schema) | Allows null | T | null |
s.union(a, b, ...) | Matches any of the given schemas | A | B | ... |
s.enum([...values]) | Union of literal values | values[number] |
import { s } from '@agentuity/schema';
// Exact value matching
const admin = s.literal('admin');
admin.parse('admin'); // 'admin'
admin.parse('user'); // throws ValidationError
// Optional and nullable fields
const Profile = s.object({
name: s.string(),
bio: s.optional(s.string()), // string | undefined
avatar: s.nullable(s.string()), // string | null
});
// Union types
const ID = s.union(s.string(), s.number());
ID.parse('abc-123'); // OK
ID.parse(42); // OK
// Enum shorthand: creates a union of literals
const Role = s.enum(['admin', 'editor', 'viewer']);
Role.parse('admin'); // 'admin'
Role.parse('other'); // throws ValidationError
// Equivalent to:
// s.union(s.literal('admin'), s.literal('editor'), s.literal('viewer'))All schemas also have .optional() and .nullable() as chainable methods:
const maybeString = s.string().optional(); // string | undefined
const nullableNum = s.number().nullable(); // number | nullExtracting Types with s.infer
Use s.infer<typeof Schema> to extract the TypeScript type from any schema. Derive the type from the schema instead of writing a separate interface.
import { s } from '@agentuity/schema';
const UserSchema = s.object({
id: s.string(),
name: s.string(),
email: s.optional(s.string()),
role: s.enum(['admin', 'editor', 'viewer']),
});
// Extract the type -- equivalent to writing the interface by hand
type User = s.infer<typeof UserSchema>;
// { id: string; name: string; email?: string; role: 'admin' | 'editor' | 'viewer' }
// Use the type in function signatures, state, or API boundaries
function greetUser(user: User): string {
return `Hello, ${user.name} (${user.role})`;
}s.infer works with all schema types, including nested objects, arrays, unions, and optional/nullable wrappers.
For non-builder schemas, the standalone Infer type export does the same thing:
import { type Infer } from '@agentuity/schema';
type User = Infer<typeof UserSchema>;Type Coercion
Coercion schemas convert input values before validating. Useful for form data, query parameters, and other string-based inputs where the runtime type doesn't match the desired type.
Coercion schemas are accessed via s.coerce.*:
| Schema | Conversion | Fails on |
|---|---|---|
s.coerce.string() | String(value) | Never (all values coerce) |
s.coerce.number() | Number(value) | NaN results |
s.coerce.boolean() | Boolean(value) | Never (uses JS truthiness) |
s.coerce.date() | new Date(value) | Invalid dates |
import { s } from '@agentuity/schema';
const QueryParams = s.object({
page: s.coerce.number(),
limit: s.coerce.number(),
active: s.coerce.boolean(),
since: s.coerce.date(),
});
// String inputs from query parameters are coerced to the correct types
const params = QueryParams.parse({
page: '2', // -> 2
limit: '25', // -> 25
active: 'true', // -> true
since: '2025-01-01', // -> Date object
});s.coerce.number() rejects values that produce NaN, and s.coerce.date() rejects invalid date strings:
s.coerce.number().parse('abc'); // throws: Cannot coerce string to number
s.coerce.date().parse('not-a-date'); // throws: Cannot coerce string to dateJSON Schema Conversion
Convert between @agentuity/schema schemas and a JSON Schema Draft 7 subset. This is useful for LLM structured output, API documentation, and interoperability with tools that consume JSON Schema.
toJSONSchema() converts a schema to a JSON Schema object:
import { s } from '@agentuity/schema';
const UserSchema = s.object({
name: s.string().describe('Full name'),
age: s.number().describe('Age in years'),
role: s.enum(['admin', 'user']),
});
const jsonSchema = s.toJSONSchema(UserSchema);
// {
// type: 'object',
// properties: {
// name: { type: 'string', description: 'Full name' },
// age: { type: 'number', description: 'Age in years' },
// role: { anyOf: [{ type: 'string', const: 'admin' }, { type: 'string', const: 'user' }] }
// },
// required: ['name', 'age', 'role']
// }For LLM structured output (OpenAI, Groq, etc.), pass { strict: true } to add additionalProperties: false to all object schemas:
const strictSchema = s.toJSONSchema(UserSchema, { strict: true });
// Adds additionalProperties: false to the object, required by some LLM providersfromJSONSchema() converts supported JSON Schema objects back into schemas, including the shapes emitted by toJSONSchema():
const jsonSchema = {
type: 'object' as const,
properties: {
name: { type: 'string' as const },
score: { type: 'number' as const },
},
required: ['name', 'score'],
};
const schema = s.fromJSONSchema(jsonSchema);
const result = schema.parse({ name: 'Alice', score: 95 });Using with AI SDK Structured Output
Use toJSONSchema() with { strict: true } when passing an @agentuity/schema schema to AI SDK structured output. The strict flag adds additionalProperties: false to every object, which providers like OpenAI require.
import { Output, generateText, jsonSchema } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { s } from '@agentuity/schema';
const SentimentSchema = s.object({
sentiment: s.enum(['positive', 'negative', 'neutral']),
confidence: s.number(),
reasoning: s.string(),
});
type Sentiment = s.infer<typeof SentimentSchema>;
const SentimentOutput = jsonSchema<Sentiment>(
s.toJSONSchema(SentimentSchema, { strict: true }),
{
validate: (value) => {
const result = SentimentSchema.safeParse(value);
return result.success
? { success: true, value: result.data }
: { success: false, error: result.error };
},
}
);
const { output } = await generateText({
model: anthropic('claude-opus-4-8'),
output: Output.object({
schema: SentimentOutput,
}),
prompt: 'Analyze the sentiment of: "I love this product!"',
});
const sentiment = output.sentiment;This approach works with any provider that accepts JSON Schema for structured output.
For supported LLM providers, see AI Gateway.
Validation Errors
Every schema provides two ways to validate data: parse() throws on failure, safeParse() returns a result object.
import { s, ValidationError } from '@agentuity/schema';
import { logger } from '@agentuity/telemetry';
const User = s.object({
name: s.string(),
age: s.number(),
});
// parse() throws a ValidationError on invalid data
try {
User.parse({ name: 123, age: 'old' });
} catch (err) {
if (err instanceof ValidationError) {
logger.info(err.message);
// [name]: Expected string, got number
// [age]: Expected number, got string
logger.info('Validation issues', err.issues);
// [
// { message: 'Expected string, got number', path: ['name'] },
// { message: 'Expected number, got string', path: ['age'] }
// ]
}
}safeParse() never throws. It returns { success: true, data } on success or { success: false, error } on failure:
const result = User.safeParse({ name: 'Alice', age: 30 });
if (result.success) {
logger.info('Parsed user', result.data); // { name: 'Alice', age: 30 }
} else {
logger.info('Validation issues', result.error.issues); // ValidationError with issues array
}ValidationError extends Error, so it works with standard error handling. Each issue includes a message and an optional path array pointing to the field that failed.