18 KiB
description, shortcut
| description | shortcut |
|---|---|
| Validate API schemas | 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:
- Choose validation library based on your tech stack
- Define validation requirements for each endpoint
- Plan error response format
- Consider performance impact for complex schemas
- 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 domainvalidators/- Compiled validation functionstypes/- Generated TypeScript types from schemasmiddleware/validation.ts- Request/response validation middlewaretests/schema.test.ts- Schema validation test suitesdocs/validation-rules.md- Documentation of all validation rules
Code Examples
Example 1: Zod Schema Validation (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<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
export type GetUserInput = z.infer<typeof getUserSchema>;
export type ListUsersInput = z.infer<typeof listUsersSchema>;
// 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)
// 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)
# 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 conversionloose: Attempt type conversionsmart: 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