9.0 KiB
name, description, allowed-tools, version
| name | description | allowed-tools | version |
|---|---|---|---|
| using-runtime-checks | Teaches how to validate external data at runtime using Zod and other validation libraries in TypeScript. Use when working with APIs, JSON parsing, user input, or any external data source where runtime validation is needed. | Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite | 1.0.0 |
- Working with API responses or external data
- Parsing JSON from unknown sources
- Handling user input or form data
- Integrating with third-party services
- User mentions validation, Zod, io-ts, runtime checks, or external data
const data: User = await fetch("/api/user").then(r => r.json());
This compiles, but if the API returns { username: string } instead of { name: string }, your code crashes at runtime.
Solution: Runtime validation libraries that:
- Validate data structure at runtime
- Provide TypeScript types automatically
- Generate helpful error messages
Recommended Library: Zod (modern, TypeScript-first, best DX)
## Runtime Validation FlowStep 1: Define Schema
Create a schema describing the expected shape:
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().int().positive()
});
Step 2: Extract TypeScript Type
type User = z.infer<typeof UserSchema>;
Step 3: Validate at Runtime
const data = await fetch("/api/user").then(r => r.json());
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
console.log(user.name);
} else {
console.error("Validation failed:", result.error);
}
Step 4: Handle Validation Errors
if (!result.success) {
const issues = result.error.issues.map(issue => ({
path: issue.path.join("."),
message: issue.message
}));
throw new ValidationError("Invalid user data", issues);
}
Setup
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "guest"]),
createdAt: z.string().datetime()
});
type User = z.infer<typeof UserSchema>;
Validation
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data: unknown = await response.json();
const result = UserSchema.safeParse(data);
if (!result.success) {
throw new Error(`Invalid user data: ${result.error.message}`);
}
return result.data;
}
Example 2: Form Input Validation
const LoginFormSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
rememberMe: z.boolean().optional()
});
type LoginForm = z.infer<typeof LoginFormSchema>;
function validateLoginForm(formData: unknown): LoginForm {
return LoginFormSchema.parse(formData);
}
const form = {
email: "user@example.com",
password: "securepassword123"
};
try {
const validated = validateLoginForm(form);
await login(validated);
} catch (error) {
if (error instanceof z.ZodError) {
error.issues.forEach(issue => {
console.error(`${issue.path.join(".")}: ${issue.message}`);
});
}
}
Example 3: Nested Object Validation
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
});
const UserWithAddressSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
address: AddressSchema,
billingAddress: AddressSchema.optional()
});
type UserWithAddress = z.infer<typeof UserWithAddressSchema>;
Example 4: Array Validation
const TagSchema = z.string().min(1).max(20);
const PostSchema = z.object({
id: z.string(),
title: z.string().min(1).max(200),
content: z.string(),
tags: z.array(TagSchema).min(1).max(10),
metadata: z.record(z.string(), z.unknown())
});
type Post = z.infer<typeof PostSchema>;
async function fetchPosts(): Promise<Post[]> {
const response = await fetch("/api/posts");
const data: unknown = await response.json();
const PostsSchema = z.array(PostSchema);
return PostsSchema.parse(data);
}
Example 5: Advanced Patterns
Union Types: Use discriminated unions for API responses with success/error states.
Transforms: Convert strings to dates or coerce query parameters to numbers.
Refinements: Add custom validation logic for complex business rules (e.g., password strength).
Partial Schemas: Create update schemas from full schemas using .partial().
See references/zod-patterns.md for complete examples of unions, transforms, refinements, and partial schemas.
For complete patterns see references/:
references/zod-patterns.md- Complete Zod API referencereferences/error-handling.md- Validation error handling strategiesreferences/performance.md- Validation performance optimization
For related skills:
- Type Guards: Use the using-type-guards skill for manual type narrowing
- Unknown vs Any: Use the avoiding-any-types skill for why validation is needed
- External Data: Use the validating-external-data skill for specific data source patterns
Cross-Plugin References:
- If constructing Zod schemas for runtime validation, use the validating-schema-basics skill for type-safe Zod v4 schema patterns
- If handling validation errors, use the customizing-errors skill for error formatting and custom messages
- Validate all external data at runtime (APIs, JSON, user input)
- Use
safeParsefor error handling, notparse(unless throwing is desired) - Handle validation errors gracefully with user-friendly messages
- Validate before type assertions
SHOULD:
- Use Zod for new projects (best TypeScript integration)
- Define schemas close to usage
- Reuse schemas for related structures
- Transform data during validation when beneficial
NEVER:
- Trust external data without validation
- Use type assertions (
as Type) on unvalidated data - Ignore validation errors
- Validate in multiple places (validate at boundaries)
- Skip validation for "trusted" sources (trust issues change)
npm install zod
pnpm add zod
yarn add zod
TypeScript configuration:
{
"compilerOptions": {
"strict": true
}
}
Schema Definition: Match expected structure, use appropriate validators, add custom refinements.
Error Handling: Use safeParse, log errors with context, provide user-friendly messages.
Type Safety: Derive types with z.infer, avoid manual assertions after validation.
Performance: Reuse schemas, validate at boundaries only, avoid redundant checks.
Testing: Test valid data, invalid data, and edge cases.
See references/error-handling.md for error handling patterns and references/performance.md for optimization techniques.
io-ts (functional programming style)
import * as t from "io-ts";
const UserCodec = t.type({
id: t.string,
name: t.string,
email: t.string
});
type User = t.TypeOf<typeof UserCodec>;
Yup (common with Formik)
import * as yup from "yup";
const userSchema = yup.object({
name: yup.string().required(),
email: yup.string().email().required()
});
AJV (JSON Schema)
import Ajv from "ajv";
const ajv = new Ajv();
const validate = ajv.compile({
type: "object",
properties: {
name: { type: "string" },
email: { type: "string", format: "email" }
},
required: ["name", "email"]
});
Recommendation: Use Zod for new TypeScript projects. Best DX and type inference.
## Common PatternsValidation Middleware: Validate request bodies in Express/framework handlers before processing.
Safe JSON Parse: Combine JSON.parse with schema validation for type-safe parsing.
Configuration Validation: Validate environment variables and config at startup.
See references/zod-patterns.md for complete implementations and additional patterns.