Skip to content

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?

  1. Runtime Safety: TypeScript types are erased at runtime. Zod provides runtime validation that catches errors before they propagate.
  2. Type Inference: Zod schemas automatically generate TypeScript types, keeping runtime and compile-time types in sync.
  3. Composability: Schemas can be composed and extended, promoting code reuse.
  4. 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:

typescript
// 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:

typescript
// 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:

typescript
// 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

typescript
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

typescript
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

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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

  1. Schema Generation: Auto-generate schemas from TypeScript interfaces
  2. Schema Documentation: Generate API documentation from schemas
  3. Schema Testing: Property-based testing using schemas
  4. 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.

Built with ❤️ for the AI Coding community, by Praney Behl