Steve Kinney

Generating Zod Schemas from OpenAPI

We can use a tool called orval to generate Zod schemas from an OpenAPI specification. We can configure Orval like so:

/* eslint-disable no-undef */
module.exports = {
  'busy-bee': {
    output: {
      client: 'zod',
      mode: 'single',
      target: './src/schemas.ts',
    },
    input: {
      target: './openapi.json',
    },
  },
};

And then when we run npx orval --config orval.config.cjs, we’ll get the following output.

/**
 * Generated by orval v7.7.0 🍺
 * Do not edit manually.
 * Busy Bee API
 * API for managing tasks
 * OpenAPI spec version: 1.0.0
 */
import { z as zod } from 'zod';

/**
 * Retrieve all tasks, optionally filtered by completion status
 * @summary Get all tasks
 */
export const getTasksQueryParams = zod.object({
  completed: zod.boolean().optional().describe('Filter tasks by completion status'),
});

export const getTasksResponseItem = zod.object({
  id: zod.number(),
  title: zod.string(),
  description: zod.string().optional(),
  completed: zod.boolean().optional(),
});
export const getTasksResponse = zod.array(getTasksResponseItem);

/**
 * Add a new task to the database
 * @summary Create a new task
 */
export const postTasksBody = zod
  .object({
    title: zod.string(),
    description: zod.string().optional(),
  })
  .describe('Data required to create a new task');

/**
 * Retrieve a single task by its ID
 * @summary Get a task by ID
 */
export const getTasksIdParams = zod.object({
  id: zod.number().describe('ID of the task to retrieve'),
});

export const getTasksIdResponse = zod
  .object({
    id: zod.number(),
    title: zod.string(),
    description: zod.string().optional(),
    completed: zod.boolean().optional(),
  })
  .describe('A task item');

/**
 * Update an existing task by its ID
 * @summary Update a task
 */
export const putTasksIdParams = zod.object({
  id: zod.number().describe('ID of the task to update'),
});

export const putTasksIdBody = zod
  .object({
    title: zod.string().optional(),
    description: zod.string().optional(),
    completed: zod.boolean().optional(),
  })
  .describe('Data for updating an existing task');

/**
 * Delete a task by its ID
 * @summary Delete a task
 */
export const deleteTasksIdParams = zod.object({
  id: zod.number().describe('ID of the task to delete'),
});

Not exactly the same as what we wrote by hand, but very, very close.