Skip to content

Zod for TypeScript Runtime Validation

Type Systems intermediate 20 min
Sources verified Dec 22

Zod brings runtime validation to TypeScript AI applications, ensuring LLM outputs match your types at runtime while maintaining compile-time type safety.

Zod is a TypeScript-first schema validation library. In AI development, it bridges the gap between TypeScript's compile-time types and runtime validation, ensuring LLM outputs are not just typed but actually validated.

The key insight: TypeScript types disappear at runtime. Zod schemas validate at runtime AND generate TypeScript types, giving you both compile-time and runtime safety.

The TypeScript Problem Zod Solves

TypeScript types are compile-time only:

interface Person {
  name: string;
  email: string;
  age: number;
}

// TypeScript thinks this is safe
const response = await fetch('/api/llm');
const person: Person = await response.json();

// But at runtime, there's NO validation!
// What if the LLM returned {"name": 123, "email": null}?
// TypeScript won't catch this — it only checks at compile time.

Zod provides runtime validation:

import { z } from 'zod';

const PersonSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive()
});

// Infer TypeScript type from schema
type Person = z.infer<typeof PersonSchema>;

const response = await fetch('/api/llm');
const data = await response.json();

// Validate at runtime
const person = PersonSchema.parse(data);
// Throws error if validation fails!
// person is now type-safe AND validated
zod_openai.ts
import { z } from 'zod';
import OpenAI from 'openai';
import { zodToJsonSchema } from 'zod-to-json-schema';

const openai = new OpenAI();

// Define schema with Zod
const SkillSchema = z.object({
  name: z.string(),
  years_experience: z.number().int().min(0),
  proficiency: z.enum(['beginner', 'intermediate', 'expert'])
});

const ResumeSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  skills: z.array(SkillSchema).min(1).max(20),
  summary: z.string().optional()
});

// Infer TypeScript types
type Skill = z.infer<typeof SkillSchema>;
type Resume = z.infer<typeof ResumeSchema>;
// Resume is now: { name: string; email: string; skills: Skill[]; summary?: string }

// Convert Zod schema to JSON Schema for OpenAI
const jsonSchema = zodToJsonSchema(ResumeSchema, 'resume');

const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [
    { role: 'user', content: 'Extract resume: John Doe, john@example.com, 5 years Python...' }
  ],
  response_format: {
    type: 'json_schema',
    json_schema: {
      name: 'resume',
      schema: jsonSchema,
      strict: true
    }
  }
});

// Parse and validate the response
const resume = ResumeSchema.parse(
  JSON.parse(response.choices[0].message.content ?? '{}')
);

// Now resume is both type-safe and runtime-validated
console.log(resume.name);  // TypeScript knows this is string
console.log(resume.email); // TypeScript knows this is string (validated email)
for (const skill of resume.skills) {  // TypeScript knows this is Skill[]
  console.log(`${skill.name}: ${skill.proficiency}`);  // Full autocomplete
}
L11: z.enum() ensures only specific values allowed
L22: z.infer extracts TypeScript type from schema
L26: Convert Zod to JSON Schema for LLM API
L44: parse() validates at runtime, throws if invalid
L49: Full type safety + runtime validation

Zod Features for AI

1. Validation Methods

const schema = z.string().email();

// parse() - throws on error
try {
  const email = schema.parse(data);
} catch (error) {
  console.error(error);
}

// safeParse() - returns result object
const result = schema.safeParse(data);
if (result.success) {
  console.log(result.data);  // validated data
} else {
  console.error(result.error);  // validation errors
}

2. Schema Composition

const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string()
});

const PersonSchema = z.object({
  name: z.string(),
  address: AddressSchema  // Nested validation
});

const CompanySchema = z.object({
  name: z.string(),
  employees: z.array(PersonSchema)  // Array of validated objects
});

3. Refinements (Custom Validation)

const PasswordSchema = z.string()
  .min(8)
  .refine(
    (val) => /[A-Z]/.test(val),
    { message: "Must contain uppercase letter" }
  )
  .refine(
    (val) => /[0-9]/.test(val),
    { message: "Must contain number" }
  );

const AgeSchema = z.number()
  .int()
  .refine(
    (val) => val >= 18 && val <= 120,
    { message: "Age must be between 18 and 120" }
  );

4. Transformations

const DateSchema = z.string().transform((str) => new Date(str));
const UppercaseSchema = z.string().transform((s) => s.toUpperCase());
const TrimmedSchema = z.string().trim();  // Built-in transform

const result = DateSchema.parse("2025-12-20");
// result is Date object, not string

5. Discriminated Unions

const ToolCallSchema = z.object({
  type: z.literal('function'),
  function_name: z.string()
});

const TextResponseSchema = z.object({
  type: z.literal('text'),
  content: z.string()
});

const ResponseSchema = z.discriminatedUnion('type', [
  ToolCallSchema,
  TextResponseSchema
]);

// Zod uses 'type' field to determine which schema to use
const response = ResponseSchema.parse(data);
if (response.type === 'function') {
  console.log(response.function_name);  // TypeScript knows this exists
} else {
  console.log(response.content);  // TypeScript knows this exists
}

Zod with Astro Content Collections

Astro (the framework powering this platform's frontend) uses Zod for content validation:

src/content/config.ts
import { z, defineCollection } from 'astro:content';

// Define schema for content files
const conceptCollection = defineCollection({
  type: 'data',
  schema: z.object({
    id: z.string(),
    name: z.string(),
    category: z.enum(['fundamentals', 'patterns', 'protocols', 'type_systems']),
    summary: z.string(),
    difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
    estimated_time: z.string(),
    tags: z.array(z.string()),
    version_added: z.string(),
    last_updated: z.string()
  })
});

export const collections = {
  concepts: conceptCollection
};

// Astro validates all content at build time
// Invalid content = build fails (catches errors before production)
L6: Zod schema defines content structure
L23: Build-time validation prevents bad content

Zod vs TypeScript Interfaces

Aspect TypeScript Interfaces Zod
Compile-time types ✓ (via z.infer)
Runtime validation
JSON Schema generation ✓ (via zod-to-json-schema)
Error messages Compile errors only Detailed runtime errors
Transformations
Custom validation ✓ (via refine)
Dynamic schemas

When to Use Zod

Use Zod When Consider Alternatives When
Validating external data (APIs, LLMs) Data is internal and type-safe
Building TypeScript AI apps Using Python (use Pydantic)
You need runtime type safety Compile-time checks are sufficient
Working with OpenAI/Anthropic APIs Simple key-value extraction
Content validation (Astro, tRPC) Performance is absolutely critical

Common Patterns

API Response Validation

import { z } from 'zod';

const ApiResponseSchema = z.object({
  success: z.boolean(),
  data: z.unknown(),  // Validated later
  error: z.string().optional()
});

const response = await fetch('/api/llm');
const json = await response.json();
const result = ApiResponseSchema.parse(json);

if (result.success) {
  const data = MyDataSchema.parse(result.data);
}

Environment Variable Validation

const EnvSchema = z.object({
  OPENAI_API_KEY: z.string().min(1),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.string().transform(Number).pipe(z.number().int().positive())
});

// Validate env vars at startup
const env = EnvSchema.parse(process.env);
// App won't start if env vars are invalid

Form Data Validation

const FormSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string()
}).refine(
  (data) => data.password === data.confirmPassword,
  { message: "Passwords don't match", path: ['confirmPassword'] }
);

const result = FormSchema.safeParse(formData);
if (!result.success) {
  console.error(result.error.flatten());
}

Key Takeaways

  • Zod provides runtime validation for TypeScript (types disappear at runtime)
  • z.infer<> extracts TypeScript types from schemas
  • zodToJsonSchema converts Zod to JSON Schema for LLM APIs
  • Use parse() for errors or safeParse() for result objects
  • Astro Content Collections use Zod for build-time validation
  • Zod schemas are composable, transformable, and dynamic

In This Platform

This platform will use Zod extensively in the Astro frontend. Content Collections validate all JSON content (dimensions, concepts, modules) at build time using Zod schemas. This ensures only valid content reaches production, catching errors early.

Relevant Files:
  • frontend/src/content/config.ts (future)
  • schema/survey.schema.json
  • schema/concept.schema.json
content/config.ts (future)
// Future: Astro Content Collections config
import { z, defineCollection } from 'astro:content';

const SourceReferenceSchema = z.object({
  id: z.string(),
  claim: z.string(),
  quote: z.string().optional(),
  page: z.string().optional()
});

const QuestionSchema = z.object({
  id: z.string(),
  text: z.string(),
  type: z.enum(['single_choice', 'multi_select', 'likert']),
  options: z.array(z.object({
    text: z.string(),
    score: z.number().int().min(0),
    sources: z.array(SourceReferenceSchema).default([])
  })).min(1),
  max_score: z.number().int(),
  sources: z.array(SourceReferenceSchema).default([])
});

const dimensionCollection = defineCollection({
  type: 'data',
  schema: z.object({
    dimension_id: z.string(),
    questions: z.array(QuestionSchema)
  })
});

export const collections = { dimensions: dimensionCollection };

// Build fails if any dimension file doesn't match schema

Prerequisites

Sources

Tempered AI Forged Through Practice, Not Hype

Keyboard Shortcuts

j
Next page
k
Previous page
h
Section home
/
Search
?
Show shortcuts
m
Toggle sidebar
Esc
Close modal
Shift+R
Reset all progress
? Keyboard shortcuts