This package is designed for legacy or incremental migrations where documentation should stay as metadata on controller methods instead of becoming application middleware. It uses @asteasolutions/zod-to-openapi under the hood and keeps the authoring experience centered on a single @openapi(...) decorator.
- Zod v4+ only
@openapi(...)method decorator- Request body shorthand for the common JSON case
- OpenAPI 3.0 and 3.1 document generation
- Re-exports
zwith.openapi(...)already enabled
bun add @devscast/zod-openapi zodIf your project uses legacy decorators, enable them in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}import { generateOpenApiDocument, openapi, z } from "@devscast/zod-openapi";
const UserParamsSchema = z.object({
user_id: z.string().min(1),
});
const PermissionsSchema = z
.object({
permissions: z.array(z.string()),
})
.openapi("Permissions");
class UsersController {
@openapi({
method: "put",
path: "/api/users/:user_id/permissions",
tags: ["Users"],
summary: "Update User Permissions",
description: "Update permissions for a specific user by their ID.",
request: {
params: UserParamsSchema,
body: PermissionsSchema,
},
responses: {
200: {
description: "Updated permissions",
content: {
"application/json": {
schema: z.object({
id: UserParamsSchema.shape.user_id,
}),
},
},
},
},
})
updatePermissions() {
return null;
}
}
const document = generateOpenApiDocument({
controllers: [UsersController],
document: {
openapi: "3.0.0",
info: {
title: "Example API",
version: "1.0.0",
},
},
});document.paths will contain /api/users/{user_id}/permissions even though the decorator used the Express-style :user_id path.
If you want to register extra components or mix manual routes with decorated ones, build a registry explicitly:
import {
OpenApiGeneratorV3,
createOpenApiRegistry,
} from "@devscast/zod-openapi";
const registry = createOpenApiRegistry({
controllers: [UsersController],
routes: [
{
method: "get",
path: "/health",
tags: ["System"],
summary: "Health check",
responses: {
200: {
description: "OK",
},
},
},
],
register(registry) {
registry.registerComponent("securitySchemes", "bearerAuth", {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
});
},
});
const document = new OpenApiGeneratorV3(registry.definitions).generateDocument({
openapi: "3.0.0",
info: {
title: "Example API",
version: "1.0.0",
},
});Use generateOpenApi31Document(...) when you want a 3.1 document:
import { generateOpenApi31Document } from "@devscast/zod-openapi";
const document = generateOpenApi31Document({
controllers: [UsersController],
document: {
openapi: "3.1.0",
info: {
title: "Example API",
version: "1.0.0",
},
},
});