Skip to main content

Overview

Hono provides built-in validation middleware to validate and transform incoming request data. The validator middleware can validate various parts of the request including JSON body, form data, query parameters, path parameters, headers, and cookies.

The Validator Middleware

The validator middleware is a flexible system for validating request data: From src/validator/validator.ts:46-88:
export const validator = <
  InputType,
  P extends string,
  M extends string,
  U extends ValidationTargetByMethod<M>,
  VF extends (
    value: unknown extends InputType ? ValidationTargets[U] : InputType,
    c: Context<any, P2>
  ) => any,
  E extends Env = any,
>(
  target: U,
  validationFunc: VF
): MiddlewareHandler<E, P, V, ExtractValidationResponse<VF>>

Validation Targets

The validator can validate different parts of the request:
  • json - Request body as JSON
  • form - Form data (multipart or urlencoded)
  • query - Query string parameters
  • param - Path parameters
  • header - Request headers
  • cookie - Request cookies
From src/validator/validator.ts:9-12:
type ValidationTargetKeysWithBody = 'form' | 'json'
type ValidationTargetByMethod<M> = M extends 'get' | 'head'
  ? Exclude<keyof ValidationTargets, ValidationTargetKeysWithBody>
  : keyof ValidationTargets
GET and HEAD requests cannot validate json or form targets since these methods must not have a body.

Basic Usage

Validating JSON Body

import { Hono } from 'hono'
import { validator } from 'hono/validator'

const app = new Hono()

app.post(
  '/users',
  validator('json', (value, c) => {
    const parsed = {
      name: value.name,
      email: value.email,
      age: parseInt(value.age)
    }
    
    if (!parsed.name || !parsed.email) {
      return c.text('Invalid data', 400)
    }
    
    return parsed
  }),
  async (c) => {
    const { name, email, age } = c.req.valid('json')
    return c.json({ message: 'User created', name, email, age })
  }
)

Validating Query Parameters

app.get(
  '/search',
  validator('query', (value, c) => {
    return {
      q: value.q || '',
      page: parseInt(value.page) || 1,
      limit: parseInt(value.limit) || 10
    }
  }),
  (c) => {
    const { q, page, limit } = c.req.valid('query')
    return c.json({ query: q, page, limit })
  }
)

Validating Path Parameters

app.get(
  '/users/:id',
  validator('param', (value, c) => {
    const id = parseInt(value.id)
    if (isNaN(id) || id <= 0) {
      return c.text('Invalid ID', 400)
    }
    return { id }
  }),
  (c) => {
    const { id } = c.req.valid('param')
    return c.json({ userId: id })
  }
)

Validating Form Data

app.post(
  '/upload',
  validator('form', (value, c) => {
    const file = value.file
    const description = value.description
    
    if (!file || !(file instanceof File)) {
      return c.text('File is required', 400)
    }
    
    return { file, description }
  }),
  async (c) => {
    const { file, description } = c.req.valid('form')
    // Process file upload
    return c.json({
      message: 'File uploaded',
      filename: file.name,
      size: file.size
    })
  }
)

Validation Function

The validation function receives the extracted value and the context: From src/validator/validator.ts:14-22:
export type ValidationFunction<
  InputType,
  OutputType,
  E extends Env = {},
  P extends string = string,
> = (
  value: InputType,
  c: Context<E, P>
) => OutputType | TypedResponse | Promise<OutputType> | Promise<TypedResponse>

Synchronous Validation

app.post(
  '/data',
  validator('json', (value, c) => {
    // Synchronous validation
    if (!value.name) {
      return c.text('Name is required', 400)
    }
    return { name: value.name }
  }),
  (c) => {
    const data = c.req.valid('json')
    return c.json(data)
  }
)

Asynchronous Validation

app.post(
  '/users',
  validator('json', async (value, c) => {
    // Async validation - check if email exists
    const exists = await checkEmailExists(value.email)
    
    if (exists) {
      return c.text('Email already exists', 409)
    }
    
    return {
      email: value.email,
      name: value.name
    }
  }),
  async (c) => {
    const user = c.req.valid('json')
    // Create user
    return c.json(user)
  }
)

Error Handling

Returning Error Responses

Return a Response object to short-circuit the request:
app.post(
  '/data',
  validator('json', (value, c) => {
    if (!value.email || !value.email.includes('@')) {
      // Return error response - stops execution
      return c.json({ error: 'Invalid email' }, 400)
    }
    return value
  }),
  (c) => {
    // This handler won't execute if validation fails
    const data = c.req.valid('json')
    return c.json({ success: true, data })
  }
)
From src/validator/validator.ts:162-170:
const res = await validationFunc(value as never, c as never)

if (res instanceof Response) {
  return res as ExtractValidationResponse<VF>
}

c.req.addValidatedData(target, res as never)

return (await next()) as ExtractValidationResponse<VF>

Throwing HTTPException

import { HTTPException } from 'hono/http-exception'

app.post(
  '/data',
  validator('json', (value, c) => {
    if (!value.name) {
      throw new HTTPException(400, { message: 'Name is required' })
    }
    return value
  }),
  (c) => {
    const data = c.req.valid('json')
    return c.json(data)
  }
)

Malformed Data Errors

The validator automatically handles malformed data: From src/validator/validator.ts:94-103:
case 'json':
  if (!contentType || !jsonRegex.test(contentType)) {
    break
  }
  try {
    value = await c.req.json()
  } catch {
    const message = 'Malformed JSON in request body'
    throw new HTTPException(400, { message })
  }
  break

Integration with Validation Libraries

Using Zod

import { z } from 'zod'
import { validator } from 'hono/validator'

const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional()
})

app.post(
  '/users',
  validator('json', (value, c) => {
    const result = userSchema.safeParse(value)
    
    if (!result.success) {
      return c.json({ error: result.error.flatten() }, 400)
    }
    
    return result.data
  }),
  (c) => {
    const user = c.req.valid('json')
    // user is typed as { name: string; email: string; age?: number }
    return c.json(user)
  }
)

Zod Validator Helper

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

const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional()
})

app.post(
  '/users',
  zValidator('json', userSchema),
  (c) => {
    const user = c.req.valid('json')
    // Fully typed based on schema
    return c.json(user)
  }
)

Multiple Validators

Chain multiple validators for different targets:
app.post(
  '/api/posts/:id/comments',
  validator('param', (value, c) => {
    const id = parseInt(value.id)
    if (isNaN(id)) {
      return c.text('Invalid post ID', 400)
    }
    return { id }
  }),
  validator('json', (value, c) => {
    if (!value.text || value.text.length < 1) {
      return c.text('Comment text is required', 400)
    }
    return { text: value.text }
  }),
  async (c) => {
    const { id } = c.req.valid('param')
    const { text } = c.req.valid('json')
    
    // Create comment
    return c.json({ postId: id, text })
  }
)

Validation Target Implementation

From src/validator/validator.ts:89-160, the validator extracts data based on target:
case 'query':
  value = Object.fromEntries(
    Object.entries(c.req.queries()).map(([k, v]) => {
      return v.length === 1 ? [k, v[0]] : [k, v]
    })
  )
  break

Form Data Handling

Form data supports both multipart and urlencoded formats: From src/validator/validator.ts:105-142:
case 'form': {
  if (
    !contentType ||
    !(multipartRegex.test(contentType) || urlencodedRegex.test(contentType))
  ) {
    break
  }

  let formData: FormData

  if (c.req.bodyCache.formData) {
    formData = await c.req.bodyCache.formData
  } else {
    try {
      const arrayBuffer = await c.req.arrayBuffer()
      formData = await bufferToFormData(arrayBuffer, contentType)
      c.req.bodyCache.formData = formData
    } catch (e) {
      let message = 'Malformed FormData request.'
      message += e instanceof Error ? ` ${e.message}` : ` ${String(e)}`
      throw new HTTPException(400, { message })
    }
  }

  const form: BodyData<{ all: true }> = Object.create(null)
  formData.forEach((value, key) => {
    if (key.endsWith('[]')) {
      ((form[key] ??= []) as unknown[]).push(value)
    } else if (Array.isArray(form[key])) {
      (form[key] as unknown[]).push(value)
    } else if (Object.hasOwn(form, key)) {
      form[key] = [form[key] as string | File, value]
    } else {
      form[key] = value
    }
  })
  value = form
  break
}

Form Arrays

app.post(
  '/form',
  validator('form', (value, c) => {
    // Fields ending with [] are treated as arrays
    return {
      tags: value['tags[]'], // Array of values
      name: value.name // Single value
    }
  }),
  (c) => {
    const data = c.req.valid('form')
    return c.json(data)
  }
)

Type Safety

Validators provide full type safety:
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const postSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string(),
  published: z.boolean().default(false),
  tags: z.array(z.string()).optional()
})

app.post(
  '/posts',
  zValidator('json', postSchema),
  (c) => {
    const post = c.req.valid('json')
    // post is typed as:
    // {
    //   title: string
    //   content: string
    //   published: boolean
    //   tags?: string[]
    // }
    return c.json(post)
  }
)

Accessing Validated Data

Access validated data using c.req.valid():
app.post(
  '/users',
  validator('json', (value) => ({
    name: value.name,
    email: value.email
  })),
  (c) => {
    // Access validated JSON data
    const user = c.req.valid('json')
    
    return c.json(user)
  }
)

Custom Validators

Create reusable validator functions:
import { validator } from 'hono/validator'
import type { ValidationFunction } from 'hono/validator'

function emailValidator<P extends string>() {
  return validator('json', (value, c) => {
    const email = value.email
    
    if (!email || typeof email !== 'string') {
      return c.text('Email is required', 400)
    }
    
    if (!email.includes('@')) {
      return c.text('Invalid email format', 400)
    }
    
    return { email: email.toLowerCase() }
  })
}

app.post('/register', emailValidator(), (c) => {
  const { email } = c.req.valid('json')
  return c.json({ email })
})

Best Practices

  • Use validation libraries like Zod for complex schemas
  • Return specific error messages to help API consumers
  • Validate and transform data in one step
  • Use TypeScript to ensure type safety
  • Chain validators for multiple targets (params, query, body)
  • Keep validators focused and reusable
  • GET and HEAD requests cannot validate json or form targets
  • Always handle validation errors with appropriate status codes
  • Don’t trust client input - always validate server-side

Validation Patterns

Pagination Validation

app.get(
  '/items',
  validator('query', (value, c) => {
    const page = parseInt(value.page) || 1
    const limit = parseInt(value.limit) || 10
    
    if (page < 1) {
      return c.text('Page must be >= 1', 400)
    }
    
    if (limit < 1 || limit > 100) {
      return c.text('Limit must be between 1 and 100', 400)
    }
    
    return { page, limit }
  }),
  (c) => {
    const { page, limit } = c.req.valid('query')
    return c.json({ page, limit })
  }
)

Conditional Validation

app.post(
  '/users',
  validator('json', (value, c) => {
    const data: any = {
      name: value.name,
      email: value.email
    }
    
    // Conditional validation
    if (value.age !== undefined) {
      const age = parseInt(value.age)
      if (isNaN(age) || age < 0) {
        return c.text('Invalid age', 400)
      }
      data.age = age
    }
    
    return data
  }),
  (c) => {
    const user = c.req.valid('json')
    return c.json(user)
  }
)
  • Handlers - Learn about request handlers
  • Middleware - Understand middleware execution
  • Context - Access request and response data

Build docs developers (and LLMs) love