Get the FREE Ultimate OpenClaw Setup Guide →

zod-validation

npx machina-cli add skill smicolon/ai-kit/zod-validation --openclaw
Files (1)
SKILL.md
7.5 KB

Zod Validation in Hono

Patterns for request validation using Zod and @hono/zod-validator.

Setup

bun add zod @hono/zod-validator

Basic Validation

JSON Body

import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const createUserSchema = z.object({
  email: z.string().email('Invalid email address'),
  name: z.string().min(1, 'Name is required').max(100),
  age: z.number().int().positive().optional(),
})

app.post('/users',
  zValidator('json', createUserSchema),
  async (c) => {
    const data = c.req.valid('json')
    // data is typed as { email: string; name: string; age?: number }
    return c.json(data, 201)
  }
)

Query Parameters

const paginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(['asc', 'desc']).default('desc'),
  search: z.string().optional(),
})

app.get('/users',
  zValidator('query', paginationSchema),
  async (c) => {
    const { page, limit, sort, search } = c.req.valid('query')
    // All values are properly typed and coerced
    return c.json({ page, limit, sort, search })
  }
)

Path Parameters

const userParamsSchema = z.object({
  id: z.string().uuid('Invalid user ID format'),
})

app.get('/users/:id',
  zValidator('param', userParamsSchema),
  async (c) => {
    const { id } = c.req.valid('param')
    return c.json({ id })
  }
)

Headers

const authHeaderSchema = z.object({
  authorization: z.string().startsWith('Bearer '),
  'x-request-id': z.string().uuid().optional(),
})

app.get('/protected',
  zValidator('header', authHeaderSchema),
  async (c) => {
    const headers = c.req.valid('header')
    return c.json({ authenticated: true })
  }
)

Form Data

const uploadSchema = z.object({
  title: z.string().min(1),
  description: z.string().optional(),
  // File validation happens separately
})

app.post('/upload',
  zValidator('form', uploadSchema),
  async (c) => {
    const { title, description } = c.req.valid('form')
    return c.json({ title, description })
  }
)

Schema Patterns

Reusable Field Schemas

// validators/common.ts
export const emailSchema = z.string().email('Invalid email')
export const uuidSchema = z.string().uuid('Invalid ID format')
export const dateSchema = z.coerce.date()

export const paginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
})

Create/Update Pattern

// validators/user.schema.ts
export const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  password: z.string().min(8),
  role: z.enum(['user', 'admin']).default('user'),
})

// Partial for updates (all fields optional)
export const updateUserSchema = createUserSchema.partial()

// Omit for specific updates
export const updatePasswordSchema = createUserSchema.pick({
  password: true,
}).extend({
  currentPassword: z.string(),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
})

// Infer TypeScript types
export type CreateUser = z.infer<typeof createUserSchema>
export type UpdateUser = z.infer<typeof updateUserSchema>

Nested Objects

const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string(),
  zip: z.string(),
})

const orderSchema = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive(),
  })).min(1, 'At least one item required'),
  shippingAddress: addressSchema,
  billingAddress: addressSchema.optional(),
})

Conditional Validation

const paymentSchema = z.discriminatedUnion('method', [
  z.object({
    method: z.literal('card'),
    cardNumber: z.string().length(16),
    cvv: z.string().length(3),
  }),
  z.object({
    method: z.literal('paypal'),
    paypalEmail: z.string().email(),
  }),
  z.object({
    method: z.literal('bank'),
    accountNumber: z.string(),
    routingNumber: z.string(),
  }),
])

Custom Refinements

const registrationSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords must match',
  path: ['confirmPassword'],
})

const dateRangeSchema = z.object({
  startDate: z.coerce.date(),
  endDate: z.coerce.date(),
}).refine(data => data.endDate > data.startDate, {
  message: 'End date must be after start date',
  path: ['endDate'],
})

Transform

const userInputSchema = z.object({
  email: z.string().email().toLowerCase().trim(),
  name: z.string().trim(),
  tags: z.string().transform(s => s.split(',').map(t => t.trim())),
})

Custom Error Handling

Custom Error Hook

import { zValidator } from '@hono/zod-validator'

const customValidator = <T extends z.ZodType>(
  target: 'json' | 'query' | 'param' | 'header' | 'form',
  schema: T
) => {
  return zValidator(target, schema, (result, c) => {
    if (!result.success) {
      const errors = result.error.issues.map(issue => ({
        field: issue.path.join('.'),
        message: issue.message,
      }))

      return c.json({
        error: 'Validation failed',
        details: errors,
      }, 400)
    }
  })
}

// Usage
app.post('/users',
  customValidator('json', createUserSchema),
  async (c) => {
    const data = c.req.valid('json')
    return c.json(data)
  }
)

Validation Error Response Format

// Standard error format
{
  "error": "Validation failed",
  "details": [
    { "field": "email", "message": "Invalid email address" },
    { "field": "name", "message": "Name is required" }
  ]
}

Multiple Validators

Chain validators for different request parts:

app.put('/users/:id',
  zValidator('param', userParamsSchema),
  zValidator('json', updateUserSchema),
  async (c) => {
    const { id } = c.req.valid('param')
    const data = c.req.valid('json')

    return c.json({ id, ...data })
  }
)

Type Export Pattern

// validators/user.schema.ts
import { z } from 'zod'

export const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string(),
})

export const updateUserSchema = createUserSchema.partial()

export const userParamsSchema = z.object({
  id: z.string().uuid(),
})

export const userQuerySchema = z.object({
  page: z.coerce.number().default(1),
  limit: z.coerce.number().default(20),
})

// Export inferred types
export type CreateUser = z.infer<typeof createUserSchema>
export type UpdateUser = z.infer<typeof updateUserSchema>
export type UserParams = z.infer<typeof userParamsSchema>
export type UserQuery = z.infer<typeof userQuerySchema>

Best Practices

  1. Always use zValidator for all request inputs
  2. Use z.coerce for query params (they're always strings)
  3. Provide clear error messages in schema definitions
  4. Export inferred types for use elsewhere
  5. Create reusable field schemas for common patterns
  6. Use .partial() for update schemas
  7. Use .refine() for cross-field validation
  8. Set sensible defaults with .default()

Source

git clone https://github.com/smicolon/ai-kit/blob/main/packs/hono/skills/zod-validation/SKILL.mdView on GitHub

Overview

Zod Validation in Hono provides patterns for validating JSON bodies, query parameters, path parameters, and headers using @hono/zod-validator. It enforces strong typing and clear error handling to prevent invalid requests from reaching your handlers.

How This Skill Works

Define Zod schemas for each input part and attach zValidator(type, schema) to routes (json, query, param, header, or form). On success, c.req.valid(type) returns typed data; on failure, the validator returns a structured error response that you can surface to the client.

When to Use It

  • Validating a JSON body for create/update operations (e.g., POST /users with a createUserSchema).
  • Validating query parameters for pagination and filtering (e.g., GET /users with page/limit/sort).
  • Validating path parameters like IDs (e.g., GET /users/:id with an id schema).
  • Validating required authentication headers (e.g., authorization header) before processing a request.
  • Validating form data for file uploads or form submissions (e.g., POST /upload with a form schema).

Quick Start

  1. Step 1: Install dependencies (bun add zod @hono/zod-validator).
  2. Step 2: Define a Zod schema and attach zValidator to a route (e.g., zValidator('json', schema)).
  3. Step 3: Read validated data with c.req.valid('json'|'query'|'param'|'header'|'form') and handle errors.

Best Practices

  • Define precise, domain-specific schemas for each request section (json, query, param, header, form).
  • Use z.coerce for query params where numeric or boolean types are expected to ensure proper typing.
  • Centralize shared validators (e.g., email or UUID) in a common validators file.
  • Leverage .default() and .optional() thoughtfully to provide sane defaults without masking errors.
  • Write targeted tests to cover valid and invalid inputs and verify helpful error messages.

Example Use Cases

  • POST /users with a JSON body validated by createUserSchema (email, name, age).
  • GET /users?page=2&limit=50&sort=asc with a validated paginationSchema.
  • GET /users/:id with a path param schema ensuring a UUID format.
  • GET /protected with a validated Authorization header (and optional x-request-id).
  • POST /upload with a form payload validated by uploadSchema (title, description).

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers