Repository: nestjs/swagger
Pull request #3424 (2025-04-30 14:18:43)

feat(document-builder): add support for setting extensions inside the info object
## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?

Currently, the extensions are only allowed at root level of the openapi document. According to the [OpenAPI documentation](https://swagger.io/docs/specification/v3_0/openapi-extensions/), there are more sections where this extensions can be added.

In my case, I want to add Redocly extensions like the [x-logo](https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-logo#x-logo) that is supposed to go inside the info section.


## What is the new behavior?

Adds a new parameter to the `addExtension` method (default to `root`) that also allows setting extensions inside the info section.

## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information

OpenApi extensions documentation - https://swagger.io/docs/specification/v3_0/openapi-extensions/
Redocly Info Object documentation - https://redocly.com/docs/redoc/redoc-vendor-extensions#info-object




Patch:
diff --git a/lib/document-builder.ts b/lib/document-builder.ts
index 738058aa..c7dc7a4e 100644
--- a/lib/document-builder.ts
+++ b/lib/document-builder.ts
@@ -4,6 +4,7 @@ import { ApiResponseOptions } from './decorators/api-response.decorator';
 import { buildDocumentBase } from './fixtures/document.base';
 import { OpenAPIObject } from './interfaces';
 import {
+  ExtensionLocation,
   ExternalDocumentationObject,
   ParameterObject,
   SecurityRequirementObject,
@@ -76,6 +77,9 @@ export class DocumentBuilder {
     return this;
   }
 
+  /**
+   * @deprecated Use `addServer` method to set up multiple different paths. The `setBasePath` method has been deprecated. Now, a global prefix is populated automatically. If you want to ignore it, take a look here: https://docs.nestjs.com/recipes/swagger#global-prefix.
+   */
   public setBasePath(path: string) {
     this.logger.warn(
       'The "setBasePath" method has been deprecated. Now, a global prefix is populated automatically. If you want to ignore it, take a look here: https://docs.nestjs.com/recipes/swagger#global-prefix. Alternatively, you can use "addServer" method to set up multiple different paths.'
@@ -101,14 +105,22 @@ export class DocumentBuilder {
     return this;
   }
 
-  public addExtension(extensionKey: string, extensionProperties: any): this {
+  public addExtension(
+    extensionKey: string,
+    extensionProperties: any,
+    location: ExtensionLocation = 'root'
+  ): this {
     if (!extensionKey.startsWith('x-')) {
       throw new Error(
         'Extension key is not prefixed. Please ensure you prefix it with `x-`.'
       );
     }
 
-    this.document[extensionKey] = clone(extensionProperties);
+    if (location === 'root') {
+      this.document[extensionKey] = clone(extensionProperties);
+    } else {
+      this.document[location][extensionKey] = clone(extensionProperties);
+    }
 
     return this;
   }
diff --git a/lib/interfaces/open-api-spec.interface.ts b/lib/interfaces/open-api-spec.interface.ts
index 9037a126..c004e354 100644
--- a/lib/interfaces/open-api-spec.interface.ts
+++ b/lib/interfaces/open-api-spec.interface.ts
@@ -280,3 +280,5 @@ export interface OAuthFlowObject {
 
 export type ScopesObject = Record<string, any>;
 export type SecurityRequirementObject = Record<string, string[]>;
+
+export type ExtensionLocation = 'root' | 'info';


Test patch:
diff --git a/e2e/api-spec.json b/e2e/api-spec.json
index faa2c971..d4ef615d 100644
--- a/e2e/api-spec.json
+++ b/e2e/api-spec.json
@@ -1684,7 +1684,10 @@
     "title": "Cats example",
     "description": "The cats API description",
     "version": "1.0",
-    "contact": {}
+    "contact": {},
+    "x-logo": {
+      "url": "https://example.com/logo.png"
+    }
   },
   "tags": [
     {
@@ -2114,5 +2117,8 @@
       "basic": [],
       "cookie": []
     }
-  ]
+  ],
+  "x-test": {
+    "test": "test"
+  }
 }
\ No newline at end of file
diff --git a/e2e/validate-schema.e2e-spec.ts b/e2e/validate-schema.e2e-spec.ts
index cc62930a..c1550500 100644
--- a/e2e/validate-schema.e2e-spec.ts
+++ b/e2e/validate-schema.e2e-spec.ts
@@ -64,6 +64,8 @@ describe('Validate OpenAPI schema', () => {
         in: 'header',
         schema: { type: 'string' }
       })
+      .addExtension('x-test', { test: 'test' })
+      .addExtension('x-logo', { url: 'https://example.com/logo.png' }, 'info')
       .build();
   });
 
@@ -219,7 +221,7 @@ describe('Validate OpenAPI schema', () => {
     expect(api.components.schemas).toHaveProperty('Cat');
   });
 
-  it('should consider explicit config over auto-detected schema', async () => {
+  it('should consider explicit config over auto-detected schema', () => {
     const document = SwaggerModule.createDocument(app, options);
     expect(document.paths['/api/cats/download'].get.responses).toEqual({
       '200': {
@@ -234,10 +236,30 @@ describe('Validate OpenAPI schema', () => {
     });
   });
 
-  it('should not add optional properties to required list', async () => {
+  it('should not add optional properties to required list', () => {
     const document = SwaggerModule.createDocument(app, options);
     const required = (document.components?.schemas?.Cat as SchemaObject)
       ?.required;
     expect(required).not.toContain('optionalRawDefinition');
   });
+
+  it('should fail if extension is not prefixed with x-', () => {
+    expect(() =>
+      new DocumentBuilder().addExtension('test', { test: 'test' }).build()
+    ).toThrow(
+      'Extension key is not prefixed. Please ensure you prefix it with `x-`.'
+    );
+  });
+
+  it('should add extension to root', () => {
+    const document = SwaggerModule.createDocument(app, options);
+    expect(document['x-test']).toEqual({ test: 'test' });
+  });
+
+  it('should add extension to info', () => {
+    const document = SwaggerModule.createDocument(app, options);
+    expect(document.info['x-logo']).toEqual({
+      url: 'https://example.com/logo.png'
+    });
+  });
 });