What's Changed
@nestjs/swagger is now a native ES module, requires Nest 12, and changes how nullable schemas are spelled in the generated document.
ESM migration
The package is published as pure ESM ("type": "module", compiled with NodeNext) behind a proper exports map. The legacy root index.ts / plugin.js / plugin.ts shims are gone, and deep imports into build internals are no longer resolvable — import from the package root (@nestjs/swagger) or from @nestjs/swagger/plugin.
require(esm) — CommonJS still works
You do not need to convert your app to ESM. Thanks to Node's require(esm) support, a CommonJS app can keep doing const { SwaggerModule } = require('@nestjs/swagger'). The CLI plugin entry (@nestjs/swagger/plugin) also keeps a require condition so nest-cli.json setups load it unchanged.
This is why the package now declares "engines": { "node": "^20.19.0 || >=22.12.0" } — those are the Node versions where require(esm) is available without a flag.
Nest 12 peer dependencies
@nestjs/common and @nestjs/core peers are now ^12.0.0. @nestjs/mapped-types moves to 12.0.0 (itself ESM, with its major aligned to the Nest 12 line), so PartialType, PickType, OmitType and IntersectionType come from an ESM build too.
Standard Schema support
Schemas passed to Nest 12's route decorators (for example @Body({ schema: z.object({ ... }) })) can now be reflected into the OpenAPI document. Supply an adapter via the new standardSchemaConverter document option:
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import type { SwaggerDocumentOptions } from '@nestjs/swagger';
import { createSchema } from 'zod-openapi';
import type { ZodType } from 'zod';
// Standard Schema exposes the producing library under `~standard.vendor`,
// which is how you narrow the raw value to a library-specific type.
function isZodSchema(schema: unknown): schema is ZodType {
return (
!!schema &&
typeof schema === 'object' &&
(schema as { '~standard'?: { vendor?: string } })['~standard']?.vendor ===
'zod'
);
}
const options: SwaggerDocumentOptions = {
standardSchemaConverter: (schema, { schemaType }) => {
if (isZodSchema(schema)) {
const { schema: converted, components } = createSchema(schema, {
io: schemaType,
openapiVersion: '3.0.0'
});
return { schema: converted, components };
}
}
};
SwaggerModule.createDocument(app, config, options);SwaggerDocumentOptions, StandardSchemaConverter and StandardSchemaConversionResult are all exported from @nestjs/swagger; createSchema comes from [zod-openapi](https://www.npmjs.com/package/zod-openapi) (for Valibot, use toJsonSchema from @valibot/to-json-schema with target: 'openapi-3.0' and check for the 'valibot' vendor instead). Neither is a dependency of this package — install whichever converter matches the schema library you use.
The callback receives the raw schema value plus whether an input or output schema is wanted, so you can narrow to library-specific types without unsafe casts, and return extra components to register. Returning undefined falls back to the DTO-derived schema, so one converter can handle several libraries and ignore the rest. Standard Schema overrides apply to bodies, queries, params, unions and enums, and take priority over the DTO-derived schema.
Breaking: nullability is spelled per document version
Nullable schemas are now normalized once on the finished document, matching the version it declares:
- 3.1.0 and later — the
nullablekeyword (removed in JSON Schema 2020-12) is gone. Typed schemas become a type union (type: ['string', 'null']), enums gain anullvalue, and references and composite schemas becomeanyOf: [<schema>, { type: 'null' }]. The 3.0type: 'object'+allOfwrapper around nullable references is unwrapped. Previously these documents carriednullable, which strict 3.1 consumers silently ignore — reading the property as non-nullable. - 3.0.x — nullable responses go back to the
nullablekeyword (with theallOfwrapper for references). Since #3897 they emittedoneOf: [<schema>, { type: 'null' }], atype: 'null'that 3.0 does not define.
The pass covers schema properties, parameters, headers, request bodies, responses, callbacks and webhooks, plus any nullable you wrote by hand. Free-form positions (example, examples, default, const, enum) and x- extensions are left alone. Snapshot tests asserting nullable: true in 3.1 documents, or oneOf in 3.0 responses, will need updating.
Closes #4063.
Breaking: lodash replaced with es-toolkit
lodash is no longer a runtime dependency — internals use es-toolkit/compat. This shrinks the install footprint and only affects you if you relied on lodash arriving transitively.
CLI plugin
esmCompatibleis now auto-detected per file. The plugin resolves each source file's implied module format (viapackage.jsontypeand themodulesetting) and emits ESM-compatible output for ESM projects. SettingesmCompatibleexplicitly innest-cli.jsonstill wins — the resolved value is only used when you left it unset. Fixes generated imports in ESM projects that previously got CJS-shaped output.- JSDoc
@paramtags now become descriptions. WithintrospectCommentson, a@paramtag is matched to the route parameter by name and sets the description on the generated@ApiQuery/@ApiParam. Existing explicit@ApiQuery/@ApiParamdecorators are left untouched. Closes #2784. - A
requireexport condition was added for the plugin entry so CJS-based CLI setups keep working. Fixes #3944.
Upgrading
For most apps the upgrade is: bump @nestjs/swagger to ^12.0.0 alongside Nest 12, make sure you are on Node 20.19+ / 22.12+, and re-check any committed OpenAPI snapshot for the nullable spelling above.