Skip to content

Zod-openAPI refinements #1275

@maelp

Description

@maelp

What is the feature you are proposing?

When I use drizzle to declare a table, then drizzle-zod to extract schemas as zod, and zod-openapi to show the zod schemas on my OpenAPI file, I'd like to be able to use the hono-openapi feature of using z.openapi("description")

for now the only hack I found is something like this in order to share my refinements in a typed way between createSchema/insertSchema/updateSchema etc

import { z } from "@hono/zod-openapi";
import { entitiesPgSchema, uuidIdColumn } from "@workspace/db/schema/shared";
import { boolean, text, timestamp } from "drizzle-orm/pg-core";
import {
  createInsertSchema,
  createSelectSchema,
  createUpdateSchema,
} from "drizzle-zod";
import { ZodTypeAny } from "zod/v4";

// Battery config type enum
export const batteryConfigTypeEnum = entitiesPgSchema.enum(
  "battery_config_type",
  ["bike_controller", "cell", "pack"],
);

export type BatteryConfigType =
  (typeof batteryConfigTypeEnum.enumValues)[number];

// Battery configs table
export const batteryConfigsTable = entitiesPgSchema.table("battery_configs", {
  id: uuidIdColumn,
  name: text("name").notNull(),
  type: batteryConfigTypeEnum("type").notNull(),
  description: text("description"),
  tags: text("tags"), // comma-separated string
  payload: text("payload").notNull(), // configuration data
  is_active: boolean("is_active").notNull().default(true),
  created_at: timestamp("created_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
  updated_at: timestamp("updated_at", { withTimezone: true })
    .defaultNow()
    .notNull()
    .$onUpdate(() => new Date()),
});

// Utility function to create refinements with better type inference
function createRefinements<T extends Record<string, any>>(
  _table: T,
  refinements: Partial<{ [K in keyof T]: (schema: T[K]) => T[K] }>,
): Partial<{ [K in keyof T]: (schema: T[K]) => T[K] }> {
  return refinements;
}

const batteryConfigRefinements = createRefinements(
  createSelectSchema(batteryConfigsTable).shape,
  {
    id: (schema) =>
      schema.openapi({
        description: "Unique identifier for the battery configuration",
      }),
    name: (schema) =>
      schema.openapi({
        description: "Name of the battery configuration",
      }),
    type: (schema) =>
      schema.openapi({
        description:
          "Type of battery configuration (bike_controller, cell, pack)",
      }),
    description: (schema) =>
      schema.openapi({
        description: "Description of the battery configuration",
      }),
    tags: (schema) =>
      schema.openapi({
        description: "Comma-separated tags for the configuration",
      }),
    payload: (schema) =>
      schema.openapi({
        description: "Configuration data payload",
      }),
    is_active: (schema) =>
      schema.openapi({
        description: "Whether the configuration is currently active",
      }),
    created_at: (schema) =>
      schema.openapi({
        description: "Timestamp when the configuration was created",
      }),
    updated_at: (schema) =>
      schema.openapi({
        description: "Timestamp when the configuration was last updated",
      }),
  },
);

// Create Zod schemas
export const selectBatteryConfigSchema = createSelectSchema(
  batteryConfigsTable,
  batteryConfigRefinements as Partial<
    Record<
      keyof (typeof batteryConfigsTable)["_"]["columns"],
      (schema: ZodTypeAny) => ZodTypeAny
    >
  >,
);

export const insertBatteryConfigSchema = createInsertSchema(
  batteryConfigsTable,
  batteryConfigRefinements as Partial<
    Record<
      keyof (typeof batteryConfigsTable)["_"]["columns"],
      (schema: ZodTypeAny) => ZodTypeAny
    >
  >,
).omit({
  id: true,
  created_at: true,
  updated_at: true,
});

export const patchBatteryConfigSchema = createUpdateSchema(
  batteryConfigsTable,
  batteryConfigRefinements as Partial<
    Record<
      keyof (typeof batteryConfigsTable)["_"]["columns"],
      (schema: ZodTypeAny) => ZodTypeAny
    >
  >,
).omit({
  id: true,
  created_at: true,
  updated_at: true,
});

// Export TypeScript types
export type BatteryConfig = z.infer<typeof selectBatteryConfigSchema>;
export type NewBatteryConfig = z.infer<typeof insertBatteryConfigSchema>;
export type PatchBatteryConfig = z.infer<typeof patchBatteryConfigSchema>;

This seems to type correctly, but it feels like there should be an easier way to do the following:

  • define a pgTable
  • get the equivalent zod type of the table
  • add openapi description for all the fields
  • use those shared openapi descriptions for createInsertSchema/createUpdateSchema/createSelectSchema

all of this well-typed

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions