or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

core-openapi.mdindex.mdopenapi-generation.mdroute-creation.mdvalidation-error-handling.md
tile.json

validation-error-handling.mddocs/

Validation & Error Handling

Validation hooks and error handling patterns for processing request validation results and generating appropriate error responses when Zod validation fails.

Capabilities

Hook Function Type

Validation hook function that processes validation results and can return custom error responses.

/**
 * Hook function for handling validation results
 * @template T - Input data type being validated
 * @template E - Environment type for context variables  
 * @template P - Path string type
 * @template R - Return type (response)
 */
type Hook<T, E extends Env, P extends string, R> = (
  result: { target: keyof ValidationTargets } & (
    | { success: true; data: T }
    | { success: false; error: ZodError }
  ),
  c: Context<E, P>
) => R;

/** Validation targets that can be hooked */
interface ValidationTargets {
  json: any;
  form: any;
  query: any;
  param: any;
  header: any;
  cookie: any;
}

Usage Examples:

import { ZodError } from "zod";

// Basic error hook
const errorHook: Hook<any, Env, string, Response | void> = (result, c) => {
  if (!result.success) {
    return c.json({
      error: 'Validation failed',
      issues: result.error.issues,
    }, 400);
  }
  // Return nothing on success - continue to handler
};

// Detailed error formatting
const detailedErrorHook = (result, c) => {
  if (!result.success) {
    const formattedErrors = result.error.issues.map(issue => ({
      field: issue.path.join('.'),
      message: issue.message,
      code: issue.code,
    }));
    
    return c.json({
      success: false,
      errors: formattedErrors,
      target: result.target, // 'json', 'query', 'param', etc.
    }, 422);
  }
};

Route-Level Hooks

Apply validation hooks to individual routes, overriding any default hook.

/**
 * Route-specific hook type inferred from route configuration
 */
type RouteHook<R extends RouteConfig> = Hook<
  InputTypeParam<R> & InputTypeQuery<R> & InputTypeHeader<R> & 
  InputTypeCookie<R> & InputTypeForm<R> & InputTypeJson<R>,
  RouteConfigToEnv<R>,
  ConvertPathType<R['path']>,
  RouteConfigToTypedResponse<R> | Response | Promise<Response> | void | Promise<void>
>;

Usage Examples:

import { createRoute, z } from "@hono/zod-openapi";

const route = createRoute({
  method: 'post',
  path: '/users',
  request: {
    body: {
      content: {
        'application/json': {
          schema: z.object({
            name: z.string().min(2),
            email: z.string().email(),
          }),
        },
      },
    },
  },
  responses: {
    201: {
      content: {
        'application/json': {
          schema: z.object({
            id: z.string(),
            name: z.string(),
            email: z.string(),
          }),
        },
      },
      description: 'User created',
    },
  },
});

// Register route with custom hook
app.openapi(
  route,
  (c) => {
    const userData = c.req.valid('json'); // Validated data
    const newUser = { id: '123', ...userData };
    return c.json(newUser, 201);
  },
  // Custom validation hook
  (result, c) => {
    if (!result.success) {
      // Custom error response for this route
      return c.json({
        error: 'User validation failed',
        details: result.error.issues,
        timestamp: new Date().toISOString(),
      }, 400);
    }
  }
);

Default Hooks

Set a default validation hook that applies to all routes unless overridden.

/**
 * OpenAPIHono constructor options with default hook
 */
interface OpenAPIHonoOptions<E extends Env> {
  /** Default validation error handler applied to all routes */
  defaultHook?: Hook<any, E, any, any>;
}

type HonoInit<E extends Env> = ConstructorParameters<typeof Hono>[0] & OpenAPIHonoOptions<E>;

Usage Examples:

// Create app with default error handling
const app = new OpenAPIHono({
  defaultHook: (result, c) => {
    if (!result.success) {
      return c.json({
        ok: false,
        errors: formatZodErrors(result.error),
        source: 'default_error_handler',
      }, 422);
    }
  },
});

// Helper function for consistent error formatting
function formatZodErrors(error: ZodError) {
  return error.issues.map(issue => ({
    path: issue.path.join('.') || 'root',
    message: issue.message,
    code: issue.code,
    received: issue.received,
  }));
}

// Routes will use default hook unless overridden
app.openapi(createPostRoute, handler); // Uses defaultHook

// Override default hook for specific route
app.openapi(
  createSpecialRoute,
  handler,
  (result, c) => {
    if (!result.success) {
      return c.json({ error: 'Special validation failed' }, 400);
    }
  }
);

Validation Targets

Different request parts that can be validated and their corresponding hook targets.

/** 
 * Validation targets corresponding to request parts
 */
interface ValidationTargets {
  /** JSON request body (Content-Type: application/json) */
  json: any;
  /** Form data (Content-Type: multipart/form-data or application/x-www-form-urlencoded) */
  form: any;
  /** URL query parameters */
  query: any;
  /** Path parameters */
  param: any;
  /** Request headers */
  header: any;
  /** Request cookies */
  cookie: any;
}

Usage Examples:

// Hook that handles different validation targets
const targetAwareHook = (result, c) => {
  if (!result.success) {
    const errorMessage = {
      json: 'Invalid request body',
      form: 'Invalid form data', 
      query: 'Invalid query parameters',
      param: 'Invalid path parameters',
      header: 'Invalid headers',
      cookie: 'Invalid cookies',
    }[result.target] || 'Validation failed';

    return c.json({
      error: errorMessage,
      target: result.target,
      issues: result.error.issues,
    }, 400);
  }
};

// Route with multiple validation targets
const complexRoute = createRoute({
  method: 'put',
  path: '/users/{id}',
  request: {
    params: z.object({ id: z.string().uuid() }),
    query: z.object({ notify: z.enum(['true', 'false']).optional() }),
    headers: z.object({ authorization: z.string() }),
    body: {
      content: {
        'application/json': {
          schema: z.object({ name: z.string().min(1) }),
        },
      },
    },
  },
  responses: {
    200: {
      content: { 'application/json': { schema: z.object({}) } },
      description: 'Updated',
    },
  },
});

app.openapi(complexRoute, handler, targetAwareHook);

Error Response Patterns

Common patterns for formatting and returning validation error responses.

Usage Examples:

// Standard REST API error format
const restErrorHook = (result, c) => {
  if (!result.success) {
    return c.json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Request validation failed',
        details: result.error.issues.map(issue => ({
          field: issue.path.join('.'),
          value: issue.received,
          message: issue.message,
        })),
      },
    }, 400);
  }
};

// Problem Details RFC 7807 format
const problemDetailsHook = (result, c) => {
  if (!result.success) {
    return c.json({
      type: 'https://example.com/problems/validation-error',
      title: 'Validation Error',
      status: 400,
      detail: `Validation failed for ${result.target}`,
      instance: c.req.path,
      errors: result.error.issues,
    }, 400);
  }
};

// Simple error message
const simpleErrorHook = (result, c) => {
  if (!result.success) {
    const firstError = result.error.issues[0];
    return c.json({
      message: firstError?.message || 'Validation failed',
    }, 400);
  }
};

// Development vs production error details
const environmentAwareHook = (result, c) => {
  if (!result.success) {
    const isDevelopment = process.env.NODE_ENV === 'development';
    
    return c.json({
      error: 'Validation failed',
      ...(isDevelopment && {
        details: result.error.issues,
        stack: result.error.stack,
      }),
    }, 400);
  }
};

Content-Type Validation Behavior

Important behavior regarding Content-Type headers and validation.

Usage Examples:

// Route with optional body - validation depends on Content-Type
const optionalBodyRoute = createRoute({
  method: 'post',
  path: '/optional-body',
  request: {
    body: {
      content: {
        'application/json': {
          schema: z.object({ data: z.string() }),
        },
      },
      // required: false (default) - only validates if Content-Type matches
    },
  },
  responses: {
    200: {
      content: { 'application/json': { schema: z.object({}) } },
      description: 'Success',
    },
  },
});

// Without proper Content-Type, c.req.valid('json') returns {}
app.openapi(optionalBodyRoute, (c) => {
  const body = c.req.valid('json'); // Could be {} if no Content-Type
  return c.json({ received: body }, 200);
});

// Force validation regardless of Content-Type
const requiredBodyRoute = createRoute({
  method: 'post',
  path: '/required-body',
  request: {
    body: {
      content: {
        'application/json': {
          schema: z.object({ data: z.string() }),
        },
      },
      required: true, // Always validates, returns error if missing Content-Type
    },
  },
  responses: {
    200: {
      content: { 'application/json': { schema: z.object({}) } },
      description: 'Success',
    },
  },
});