Decorators

OpenAPI metadata with the @Schema decorator

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

FieldSource
methodHTTP 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:

FieldPurpose
summary / descriptionHuman-readable route text
tagsGrouping in Scalar / OpenAPI UIs
requestRequest body, query, headers (often auto-filled - see below)
responsesStatus codes and response schemas
securityAuth requirements in the spec
deprecatedMark 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.

Previous

Scalar

Interactive API documentation with Scalar API Reference

Next