Zod Schema Architecture
Overview
Orchestre uses Zod for runtime type validation throughout the system. This ensures type safety at the boundaries where TypeScript's compile-time checks can't help us - namely when receiving data from external sources like AI models, user input, or file systems.
Why Zod?
- Runtime Safety: TypeScript types are erased at runtime. Zod provides runtime validation that catches errors before they propagate.
- Type Inference: Zod schemas automatically generate TypeScript types, keeping runtime and compile-time types in sync.
- Composability: Schemas can be composed and extended, promoting code reuse.
- Error Messages: Zod provides detailed error messages that help debug issues quickly.
Core Schemas
Tool Input Validation
All MCP tools validate their inputs using Zod schemas:
// src/schemas/tools.ts
export const InitializeProjectSchema = z.object({
projectName: z.string().min(1),
template: z.string(),
targetPath: z.string().optional(),
repository: z.string().url().optional(),
description: z.string().optional(),
features: z.array(z.string()).optional()
});
export const AnalyzeProjectSchema = z.object({
requirements: z.string().min(1),
context: z.object({
template: z.string().optional(),
constraints: z.array(z.string()).optional()
}).optional()
});AI Response Validation
We validate AI responses to ensure they match expected formats:
// src/schemas/ai-responses.ts
export const ProjectAnalysisSchema = z.object({
summary: z.string(),
complexity: z.enum(['simple', 'moderate', 'complex']),
techStack: z.array(z.string()),
features: z.array(z.string()),
risks: z.array(z.string()),
recommendations: z.array(z.string())
});
export const DevelopmentPlanSchema = z.object({
phases: z.array(z.object({
name: z.string(),
description: z.string(),
tasks: z.array(z.string()),
dependencies: z.array(z.string()),
estimatedDays: z.number()
})),
totalEstimatedDays: z.number(),
criticalPath: z.array(z.string())
});Configuration Validation
Project and template configurations are validated:
// src/schemas/config.ts
export const TemplateConfigSchema = z.object({
name: z.string(),
displayName: z.string(),
description: z.string(),
version: z.string(),
category: z.enum(['saas', 'api', 'mobile', 'custom']),
features: z.array(z.string()),
requirements: z.object({
node: z.string().optional(),
dependencies: z.array(z.string()).optional()
}).optional()
});
export const PromptsConfigSchema = z.object({
corePrompts: z.array(z.string()),
templatePrompts: z.array(z.string())
});Usage Patterns
1. Input Validation
export async function initializeProject(args: unknown) {
// Validate input
const validatedArgs = InitializeProjectSchema.parse(args);
// Now TypeScript knows the exact shape of validatedArgs
// and we're guaranteed it matches our schema at runtime
await createProject(validatedArgs);
}2. Safe AI Response Handling
export async function analyzeWithGemini(prompt: string) {
const response = await callGeminiAPI(prompt);
try {
// Parse and validate in one step
const analysis = ProjectAnalysisSchema.parse(response);
return { success: true, data: analysis };
} catch (error) {
// Zod provides detailed error information
console.error('Invalid AI response:', error);
return { success: false, error: error.message };
}
}3. Configuration Loading
export async function loadTemplate(templateName: string) {
const configPath = path.join(templatesDir, templateName, 'template.json');
const rawConfig = await fs.readFile(configPath, 'utf-8');
const parsedConfig = JSON.parse(rawConfig);
// Validate configuration
return TemplateConfigSchema.parse(parsedConfig);
}Best Practices
1. Define Schemas Near Usage
Keep schemas close to where they're used for better maintainability:
// In the same file as the tool
const ToolInputSchema = z.object({
// ... schema definition
});
export async function toolHandler(input: unknown) {
const validated = ToolInputSchema.parse(input);
// ... implementation
}2. Use Type Inference
Let Zod generate TypeScript types to avoid duplication:
// Define schema once
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email()
});
// Infer the type
type User = z.infer<typeof UserSchema>;
// Use the inferred type
function processUser(user: User) {
// TypeScript knows the shape
}3. Compose Complex Schemas
Build complex schemas from simpler ones:
const BaseTaskSchema = z.object({
id: z.string(),
title: z.string(),
completed: z.boolean()
});
const TaskWithMetadataSchema = BaseTaskSchema.extend({
createdAt: z.date(),
priority: z.enum(['low', 'medium', 'high']),
tags: z.array(z.string())
});4. Custom Error Messages
Provide helpful error messages for better debugging:
const ProjectNameSchema = z
.string()
.min(1, 'Project name cannot be empty')
.max(50, 'Project name must be 50 characters or less')
.regex(/^[a-zA-Z0-9-]+$/, 'Project name can only contain letters, numbers, and hyphens');Schema Locations
- Tool Schemas:
src/schemas/tools.ts - AI Response Schemas:
src/schemas/ai-responses.ts - Configuration Schemas:
src/schemas/config.ts - Prompt Schemas: Defined inline in prompt handlers
- Memory Schemas:
src/schemas/memory.ts
Error Handling
Zod errors are caught and transformed into user-friendly messages:
import { ZodError } from 'zod';
export function handleZodError(error: unknown): string {
if (error instanceof ZodError) {
const issues = error.issues.map(issue => {
const path = issue.path.join('.');
return `${path}: ${issue.message}`;
});
return `Validation failed:\n${issues.join('\n')}`;
}
return 'Unknown validation error';
}Future Enhancements
- Schema Generation: Auto-generate schemas from TypeScript interfaces
- Schema Documentation: Generate API documentation from schemas
- Schema Testing: Property-based testing using schemas
- Schema Evolution: Version schemas for backward compatibility
Conclusion
Zod provides a robust foundation for runtime type safety in Orchestre. By validating data at system boundaries, we catch errors early and provide clear feedback to users and AI models alike. The combination of runtime validation and TypeScript's static typing gives us the best of both worlds.
