agentsclimarketplace

Zod validation

Skill smicolon/ai-kit/packs/hono/skills/zod-validation

Convention packs for any AI coding tool - agents, skills, commands, and rules for 15 tools including Claude Code, Cursor, Windsurf, and Copilot

Install
npx -y skills add smicolon/ai-kit --skill zod-validation

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

This skill activates when writing form validation, request validation, or Zod schemas in Hono. It provides patterns for validating JSON bodies, query parameters, path parameters, and headers with proper error handling.

SKILL.md

7.5 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it

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()

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 327,069. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.