--- description: Validate API schemas shortcut: schema --- # Validate API Schemas Implement comprehensive schema validation using modern validation libraries like JSON Schema, Joi, Yup, or Zod to ensure type safety, data integrity, and contract compliance across your API. ## When to Use This Command Use `/validate-schemas` when you need to: - Enforce strict data types and formats in API requests and responses - Create reusable validation schemas across multiple endpoints - Generate TypeScript types from validation schemas - Implement complex conditional validation logic - Provide detailed validation error messages to clients - Ensure data consistency before database operations DON'T use this when: - Working with unstructured or highly dynamic data (use runtime checks instead) - Building quick prototypes without formal contracts (premature optimization) - Validation logic is trivial (simple type checks may suffice) ## Design Decisions This command implements **Zod** as the primary approach because: - TypeScript-first with automatic type inference - Composable schemas with chaining syntax - Zero runtime dependencies - Excellent error messages out of the box - Schema transformation and parsing capabilities - Works seamlessly with modern frameworks **Alternative considered: Joi** - More mature with extensive ecosystem - Better for JavaScript-only projects - More verbose API - Recommended for legacy Node.js applications **Alternative considered: JSON Schema** - Language-agnostic standard - Better for cross-platform validation - More complex to write and maintain - Recommended when sharing schemas across different languages ## Prerequisites Before running this command: 1. Choose validation library based on your tech stack 2. Define validation requirements for each endpoint 3. Plan error response format 4. Consider performance impact for complex schemas 5. Determine validation strategy (fail-fast vs. collect-all-errors) ## Implementation Process ### Step 1: Define Base Schemas Create reusable schema primitives for common data types and patterns. ### Step 2: Compose Endpoint Schemas Build complex schemas by composing base schemas with business rules. ### Step 3: Integrate with Middleware Set up validation middleware to automatically validate requests and responses. ### Step 4: Generate Types Generate TypeScript types from schemas for compile-time safety. ### Step 5: Implement Error Handling Create consistent error formatting and reporting mechanisms. ## Output Format The command generates: - `schemas/` - Schema definitions organized by domain - `validators/` - Compiled validation functions - `types/` - Generated TypeScript types from schemas - `middleware/validation.ts` - Request/response validation middleware - `tests/schema.test.ts` - Schema validation test suites - `docs/validation-rules.md` - Documentation of all validation rules ## Code Examples ### Example 1: Zod Schema Validation (TypeScript) ```typescript // schemas/user.schema.ts import { z } from 'zod'; // Base schemas for reuse const emailSchema = z.string().email().toLowerCase(); const passwordSchema = z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain uppercase letter') .regex(/[a-z]/, 'Password must contain lowercase letter') .regex(/[0-9]/, 'Password must contain number') .regex(/[^A-Za-z0-9]/, 'Password must contain special character'); const phoneSchema = z.string().regex( /^\+?[1-9]\d{1,14}$/, 'Invalid phone number format (E.164)' ); const addressSchema = z.object({ street: z.string().min(1).max(100), city: z.string().min(1).max(50), state: z.string().length(2).toUpperCase(), zipCode: z.string().regex(/^\d{5}(-\d{4})?$/), country: z.string().length(2).toUpperCase().default('US') }); // User schemas export const createUserSchema = z.object({ body: z.object({ email: emailSchema, password: passwordSchema, firstName: z.string().min(1).max(50), lastName: z.string().min(1).max(50), dateOfBirth: z.string().datetime().refine( (date) => { const age = new Date().getFullYear() - new Date(date).getFullYear(); return age >= 18; }, { message: 'Must be at least 18 years old' } ), phone: phoneSchema.optional(), address: addressSchema.optional(), preferences: z.object({ newsletter: z.boolean().default(false), notifications: z.enum(['email', 'sms', 'push', 'none']).default('email'), theme: z.enum(['light', 'dark', 'auto']).default('auto') }).default({}), metadata: z.record(z.string(), z.any()).optional() }) }); export const updateUserSchema = z.object({ params: z.object({ id: z.string().uuid() }), body: createUserSchema.shape.body.partial().extend({ // Can't update email without verification email: z.never().optional() }) }); export const getUserSchema = z.object({ params: z.object({ id: z.string().uuid() }) }); export const listUsersSchema = z.object({ query: z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), sort: z.enum(['createdAt', 'email', 'name']).default('createdAt'), order: z.enum(['asc', 'desc']).default('desc'), filter: z.object({ email: z.string().optional(), status: z.enum(['active', 'inactive', 'suspended']).optional(), createdAfter: z.string().datetime().optional(), createdBefore: z.string().datetime().optional() }).optional() }) }); // Type inference export type CreateUserInput = z.infer; export type UpdateUserInput = z.infer; export type GetUserInput = z.infer; export type ListUsersInput = z.infer; // Validation middleware import { Request, Response, NextFunction } from 'express'; export function validate(schema: z.ZodSchema) { return async (req: Request, res: Response, next: NextFunction) => { try { const validated = await schema.parseAsync({ body: req.body, query: req.query, params: req.params }); // Replace request properties with validated/transformed data req.body = validated.body || req.body; req.query = validated.query || req.query; req.params = validated.params || req.params; next(); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors.map(err => ({ path: err.path.join('.'), message: err.message, code: err.code })) }); } next(error); } }; } // Usage in routes import express from 'express'; const router = express.Router(); router.post('/users', validate(createUserSchema), async (req, res) => { // req.body is now typed and validated const user = await createUser(req.body); res.json(user); } ); router.put('/users/:id', validate(updateUserSchema), async (req, res) => { const user = await updateUser(req.params.id, req.body); res.json(user); } ); ``` ### Example 2: Joi Schema Validation (JavaScript) ```javascript // schemas/product.schema.js const Joi = require('joi'); // Custom validators const customValidators = { sku: Joi.string().pattern(/^[A-Z]{3}-\d{6}$/), price: Joi.number().precision(2).positive().max(999999.99), url: Joi.string().uri({ scheme: ['http', 'https'] }) }; // Product schemas const productSchema = { create: Joi.object({ sku: customValidators.sku.required(), name: Joi.string().min(3).max(200).required(), description: Joi.string().max(2000).required(), price: customValidators.price.required(), compareAtPrice: customValidators.price .greater(Joi.ref('price')) .optional(), category: Joi.string().valid( 'electronics', 'clothing', 'food', 'books', 'other' ).required(), tags: Joi.array() .items(Joi.string().min(2).max(20)) .max(10) .unique(), inventory: Joi.object({ quantity: Joi.number().integer().min(0).required(), trackInventory: Joi.boolean().default(true), allowBackorder: Joi.boolean().default(false), lowStockThreshold: Joi.number().integer().min(0).default(10) }).required(), images: Joi.array() .items(Joi.object({ url: customValidators.url.required(), alt: Joi.string().max(200), isPrimary: Joi.boolean().default(false) })) .min(1) .max(10) .unique('url') .required(), shipping: Joi.object({ weight: Joi.number().positive().required(), dimensions: Joi.object({ length: Joi.number().positive().required(), width: Joi.number().positive().required(), height: Joi.number().positive().required() }).required(), requiresShipping: Joi.boolean().default(true), shippingClass: Joi.string().valid('standard', 'fragile', 'oversized') }).when('category', { is: 'electronics', then: Joi.required(), otherwise: Joi.optional() }), variants: Joi.array() .items(Joi.object({ name: Joi.string().required(), options: Joi.array() .items(Joi.string()) .min(1) .required() })) .optional(), metadata: Joi.object().pattern( Joi.string(), Joi.alternatives().try( Joi.string(), Joi.number(), Joi.boolean() ) ).optional() }).custom((value, helpers) => { // Custom validation: ensure at least one primary image const primaryImages = value.images.filter(img => img.isPrimary); if (primaryImages.length !== 1) { return helpers.error('any.invalid', { message: 'Exactly one image must be marked as primary' }); } return value; }), update: Joi.object({ name: Joi.string().min(3).max(200), description: Joi.string().max(2000), price: customValidators.price, // ... partial schema }).min(1), // At least one field required list: Joi.object({ page: Joi.number().integer().positive().default(1), limit: Joi.number().integer().min(1).max(100).default(20), category: Joi.string(), minPrice: Joi.number().positive(), maxPrice: Joi.number().positive().greater(Joi.ref('minPrice')), search: Joi.string().max(100), inStock: Joi.boolean() }) }; // Validation middleware function validateRequest(schemaName) { return async (req, res, next) => { const schema = productSchema[schemaName]; if (!schema) { return next(new Error(`Schema ${schemaName} not found`)); } const dataToValidate = { ...req.body, ...req.query, ...req.params }; try { const validated = await schema.validateAsync(dataToValidate, { abortEarly: false, // Return all errors stripUnknown: true, // Remove unknown keys convert: true // Type coercion }); // Merge validated data back Object.keys(validated).forEach(key => { if (req.body.hasOwnProperty(key)) req.body[key] = validated[key]; if (req.query.hasOwnProperty(key)) req.query[key] = validated[key]; if (req.params.hasOwnProperty(key)) req.params[key] = validated[key]; }); next(); } catch (error) { if (error.isJoi) { return res.status(400).json({ error: 'Validation failed', details: error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message, type: detail.type })) }); } next(error); } }; } module.exports = { productSchema, validateRequest }; ``` ### Example 3: Pydantic Schema Validation (Python) ```python # schemas/order_schema.py from pydantic import BaseModel, Field, validator, root_validator from typing import List, Optional, Dict, Any from datetime import datetime, date from decimal import Decimal from enum import Enum import re class OrderStatus(str, Enum): PENDING = "pending" PROCESSING = "processing" SHIPPED = "shipped" DELIVERED = "delivered" CANCELLED = "cancelled" class PaymentMethod(str, Enum): CREDIT_CARD = "credit_card" DEBIT_CARD = "debit_card" PAYPAL = "paypal" STRIPE = "stripe" BANK_TRANSFER = "bank_transfer" class Address(BaseModel): street: str = Field(..., min_length=1, max_length=200) city: str = Field(..., min_length=1, max_length=100) state: str = Field(..., regex="^[A-Z]{2}$") zip_code: str = Field(..., regex=r"^\d{5}(-\d{4})?$") country: str = Field(default="US", regex="^[A-Z]{2}$") @validator('state', 'country') def uppercase_codes(cls, v): return v.upper() class OrderItem(BaseModel): product_id: str = Field(..., regex="^[A-Z]{3}-\d{6}$") quantity: int = Field(..., gt=0, le=100) unit_price: Decimal = Field(..., decimal_places=2, ge=0) discount: Optional[Decimal] = Field(None, decimal_places=2, ge=0, le=1) @validator('discount') def validate_discount(cls, v, values): if v and v >= 1: raise ValueError('Discount must be less than 100%') return v @property def subtotal(self) -> Decimal: discount_amount = self.discount or Decimal('0') return self.quantity * self.unit_price * (1 - discount_amount) class CreateOrderSchema(BaseModel): customer_email: str = Field(..., regex=r"^[\w\.-]+@[\w\.-]+\.\w+$") items: List[OrderItem] = Field(..., min_items=1, max_items=50) shipping_address: Address billing_address: Optional[Address] = None payment_method: PaymentMethod notes: Optional[str] = Field(None, max_length=500) coupon_code: Optional[str] = Field(None, regex="^[A-Z0-9]{4,12}$") @validator('customer_email') def validate_email(cls, v): # Additional email validation if not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", v.lower()): raise ValueError('Invalid email format') return v.lower() @validator('billing_address', always=True) def set_billing_address(cls, v, values): # Use shipping address if billing not provided return v or values.get('shipping_address') @root_validator def validate_order(cls, values): items = values.get('items', []) # Check for duplicate products product_ids = [item.product_id for item in items] if len(product_ids) != len(set(product_ids)): raise ValueError('Duplicate products in order') # Calculate total total = sum(item.subtotal for item in items) if total <= 0: raise ValueError('Order total must be positive') # Validate coupon if provided coupon = values.get('coupon_code') if coupon and not cls.validate_coupon(coupon): raise ValueError('Invalid coupon code') return values @staticmethod def validate_coupon(code: str) -> bool: # Implement coupon validation logic valid_coupons = ['SAVE10', 'FREESHIP', 'WELCOME20'] return code in valid_coupons class UpdateOrderSchema(BaseModel): status: Optional[OrderStatus] = None shipping_address: Optional[Address] = None notes: Optional[str] = Field(None, max_length=500) tracking_number: Optional[str] = Field(None, regex=r"^[A-Z0-9]{10,30}$") class Config: use_enum_values = True # FastAPI integration from fastapi import FastAPI, HTTPException, Depends from fastapi.encoders import jsonable_encoder app = FastAPI() @app.post("/orders", response_model=Dict[str, Any]) async def create_order(order: CreateOrderSchema): # Validation happens automatically order_dict = jsonable_encoder(order) # Process order return {"id": "ORD-123456", **order_dict} @app.patch("/orders/{order_id}") async def update_order( order_id: str, updates: UpdateOrderSchema ): # Only provided fields will be updated update_dict = updates.dict(exclude_unset=True) # Update order return {"id": order_id, **update_dict} # Custom validation endpoint @app.post("/validate/order") async def validate_order(order: CreateOrderSchema): # Just validate without processing return {"valid": True, "data": order.dict()} ``` ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | "Invalid type" | Wrong data type provided | Check schema definition and input data | | "Required field missing" | Mandatory field not provided | Ensure all required fields are present | | "Validation failed" | Business rule violation | Review custom validators and constraints | | "Schema not found" | Referenced schema doesn't exist | Verify schema imports and definitions | | "Circular dependency" | Schema references itself | Refactor to break circular references | ## Configuration Options **Validation Strategies** - `fail-fast`: Stop at first error (faster) - `collect-all`: Gather all errors (better UX) - `partial`: Allow partial validation for updates **Type Coercion** - `strict`: No type conversion - `loose`: Attempt type conversion - `smart`: Context-aware conversion ## Best Practices DO: - Create reusable base schemas for common patterns - Use descriptive error messages for custom validators - Generate TypeScript types from schemas - Version schemas with your API - Document all validation rules - Test edge cases and boundary conditions DON'T: - Duplicate validation logic across layers - Use overly complex nested schemas - Ignore performance impact of complex validations - Mix validation with business logic - Trust client-side validation alone ## Performance Considerations - Compile schemas once at startup - Cache validated results for identical inputs - Use async validation for external checks - Limit regex complexity to prevent ReDoS attacks - Consider schema complexity for large payloads ## Security Considerations - Sanitize error messages to prevent information leakage - Implement rate limiting on validation endpoints - Use strict type checking to prevent injection attacks - Validate file uploads separately with size limits - Never expose internal schema structure to clients ## Related Commands - `/api-response-validator` - Validate API responses - `/api-contract-generator` - Generate schemas from code - `/api-testing-framework` - Test with schema validation - `/api-documentation-generator` - Document schemas ## Version History - v1.0.0 (2024-10): Initial implementation with Zod, Joi, and Pydantic support - Planned v1.1.0: Add support for Yup and JSON Schema generation