Schema Validation — Agentuity Documentation

Schema Validation

Type-safe runtime validation with StandardSchema support

The SDK includes built-in schema validation using the StandardSchemaV1 interface, providing runtime type safety and automatic validation.

StandardSchema Support

The SDK validates agent input and output using the StandardSchemaV1 interface. Any validation library that implements this interface works out of the box: Zod, Valibot, ArkType, or the built-in @agentuity/schema package.

For choosing between schema libraries and integration patterns, see Schema Libraries. For how schemas attach to agents, see Agents.

Type Inference

TypeScript automatically infers types from your schemas, providing full autocomplete and type checking:

import { createAgent } from '@agentuity/runtime';
import { z } from 'zod';
 
const agent = createAgent('SearchAgent', {
  schema: {
    input: z.object({
      query: z.string(),
      filters: z.object({
        category: z.enum(['tech', 'business', 'sports']),
        limit: z.number().default(10),
      }),
    }),
    output: z.object({
      results: z.array(z.object({
        id: z.string(),
        title: z.string(),
        score: z.number(),
      })),
      total: z.number(),
    }),
  },
  handler: async (ctx, input) => {
    // TypeScript knows:
    // - input.query is string
    // - input.filters.category is 'tech' | 'business' | 'sports'
    // - input.filters.limit is number
 
    // Return type is also validated
    return {
      results: [
        { id: '1', title: 'Example', score: 0.95 },
      ],
      total: 1,
    };
 
    // This would cause a TypeScript error:
    // return { invalid: 'structure' };
  },
});
 
// When calling the agent from another agent:
import searchAgent from '@agent/search';
 
const result = await searchAgent.run({
  query: 'agentic AI',
  filters: { category: 'tech', limit: 5 },
});
 
// TypeScript knows result has this shape:
// {
//   results: Array<{ id: string; title: string; score: number }>;
//   total: number;
// }

Benefits of Type Inference:

  • Full IDE autocomplete for input and output
  • Compile-time type checking catches errors before runtime
  • No need to manually define TypeScript interfaces
  • Refactoring is safer - changes to schemas update types automatically

Primitive Types

The s builder provides schemas for all JavaScript primitive types.

SchemaValidatesExample
s.string()string valuess.string().parse('hello')
s.number()number valuess.number().parse(42)
s.boolean()boolean valuess.boolean().parse(true)
s.null()null onlys.null().parse(null)
s.undefined()undefined onlys.undefined().parse(undefined)
s.unknown()any value, typed as unknowns.unknown().parse('anything')
s.any()any value, typed as anys.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 ValidationError

Utility Types

Utility schemas modify or combine other schemas.

SchemaEffectOutput Type
s.literal(value)Exact value matchvalue (literal type)
s.optional(schema)Allows undefinedT | undefined
s.nullable(schema)Allows nullT | null
s.union(a, b, ...)Matches any of the given schemasA | B | ...
s.enum([...values])Union of literal valuesvalues[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 | null

Extracting Types with s.infer

Use s.infer<typeof Schema> to extract the TypeScript type from any schema. This is the recommended way to derive types rather than writing separate interfaces.

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.*:

SchemaConversionFails 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 date

JSON Schema Conversion

Convert between @agentuity/schema schemas and JSON Schema (Draft 7). 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 providers

fromJSONSchema() converts a JSON Schema object back into a schema, supporting round-trip conversion:

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 LLM Structured Output

Combine toJSONSchema() with { strict: true } to generate schemas compatible with LLM structured output APIs. The strict flag adds additionalProperties: false to every object, which providers like OpenAI require.

import { s } from '@agentuity/schema';
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
 
const SentimentSchema = s.object({
  sentiment: s.enum(['positive', 'negative', 'neutral']),
  confidence: s.number(),
  reasoning: s.string(),
});
 
const { object } = await generateObject({
  model: openai('gpt-4o'),
  schema: s.toJSONSchema(SentimentSchema, { strict: true }),
  prompt: 'Analyze the sentiment of: "I love this product!"',
});
 
// object is typed as { sentiment: string; confidence: number; reasoning: string }

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';
 
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) {
    console.log(err.message);
    // [name]: Expected string, got number
    // [age]: Expected number, got string
 
    console.log(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) {
  console.log(result.data); // { name: 'Alice', age: 30 }
} else {
  console.log(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.