Decorators
The Schema module exposes a single route decorator, @Schema, which registers OpenAPI path metadata for a controller method. It is built on @asteasolutions/zod-to-openapi RouteConfig (without method and path, which are inferred from the route).
@Schema
Apply @Schema to a method that already has an HTTP verb decorator (@Get, @Post, etc.) and @Controller base path.
import { Controller, Get } from '@vercube/core';
import { Schema, z } from '@vercube/schema';
@Controller('/items')
export class ItemsController {
@Get('/:id')
@Schema({
summary: 'Get item by ID',
tags: ['Items'],
responses: {
200: {
description: 'Item found',
content: {
'application/json': {
schema: z.object({ id: z.string(), title: z.string() }),
},
},
},
404: {
description: 'Not found',
},
},
})
getOne() {
return { id: '1', title: 'Example' };
}
}
Inferred fields
| Field | Source |
|---|---|
method | HTTP decorator on the same method (get, post, …) |
path | @Controller path + route path (e.g. /items/:id) |
You do not pass method or path to @Schema.
Options
@Schema accepts any other RouteConfig field, for example:
| Field | Purpose |
|---|---|
summary / description | Human-readable route text |
tags | Grouping in Scalar / OpenAPI UIs |
request | Request body, query, headers (often auto-filled - see below) |
responses | Status codes and response schemas |
security | Auth requirements in the spec |
deprecated | Mark route as deprecated |
@Schema({
summary: 'Upload file',
description: 'Accepts multipart uploads up to 10MB',
tags: ['Files'],
security: [{ bearerAuth: [] }],
responses: {
200: { description: 'Upload accepted' },
},
})
Automatic request schemas
When a route uses validation on parameters, the Schema module merges those schemas into the OpenAPI request object. You usually do not duplicate them in @Schema.
Request body (@Body)
If the method has @Body({ validationSchema: SomeSchema }), the body is documented as application/json with that schema:
const CreateItemSchema = z.object({
title: z.string(),
quantity: z.number().int().positive(),
});
@Post('/')
@Schema({
responses: {
201: { description: 'Created' },
},
})
create(@Body({ validationSchema: CreateItemSchema }) body: z.infer<typeof CreateItemSchema>) {
return body;
}
Generated OpenAPI includes request.body.content['application/json'].schema from CreateItemSchema.
Query parameters (@QueryParams)
If the method has @QueryParams({ validationSchema: QuerySchema }), the query object is merged into request.query:
const SearchSchema = z.object({
q: z.string().optional(),
limit: z.coerce.number().optional(),
});
@Get('/search')
@Schema({
responses: {
200: { description: 'Search results' },
},
})
search(@QueryParams({ validationSchema: SearchSchema }) query: z.infer<typeof SearchSchema>) {
return { query };
}
@QueryParam (single param) is not auto-mapped today - use @QueryParams with a Zod object for documented query strings, or describe query manually in @Schema({ request: { query: … } }).Combining manual and automatic request metadata
Manual request fields in @Schema are deep-merged with auto-resolved body/query via defu. Prefer validation schemas as the source of truth when both exist.
Response schemas
Document responses explicitly in @Schema. Use Zod schemas (with optional .openapi() metadata):
const ErrorSchema = z.object({ message: z.string() }).openapi('Error');
@Get('/risky')
@Schema({
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: z.object({ ok: z.boolean() }),
},
},
},
500: {
description: 'Server error',
content: {
'application/json': { schema: ErrorSchema },
},
},
},
})
risky() {
return { ok: true };
}
Importing z
Always import from @vercube/schema when using OpenAPI helpers:
import { Schema, z } from '@vercube/schema';
The package calls extendZodWithOpenApi(z) on load so .openapi() is available on Zod types.
Registration timing
@Schema registers paths asynchronously after the controller metadata is ready (next event-loop tick). Ensure SchemaPlugin is added before controllers are bound and the app is initialized, same as other Vercube plugins.