Repository: nestjs/swagger
Pull request #3198 (2024-12-03 16:46:46)

feat: support description in @ApiSchema
## 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)
- [x] Docs have been added / updated (for bug fixes / features) 

Related PR to update docs can be found here: 
- https://github.com/nestjs/docs.nestjs.com/pull/3157


## 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?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: [2671](https://github.com/nestjs/swagger/issues/2671)


## What is the new behavior?

When using the `@ApiSchema` decorator, it is now possible to specify the model's description.

```ts
@ApiSchema({ description: "Model Description"})
class Model {
   // ...
} 
```

When rendering the openapi spec, the description will be added to document like this:

```jsonc
{
  // ...
  "schemas": {
    "Model": {
      "type": "object",
      "description": "Model Description"
      //...
    }
  }
  // ...
}

```
 

## 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



Patch:
diff --git a/lib/decorators/api-schema.decorator.ts b/lib/decorators/api-schema.decorator.ts
index 98ea6a2b..99bb843a 100644
--- a/lib/decorators/api-schema.decorator.ts
+++ b/lib/decorators/api-schema.decorator.ts
@@ -6,9 +6,14 @@ export interface ApiSchemaOptions extends Pick<SchemaObjectMetadata, 'name'> {
   /**
    * Name of the schema.
    */
-  name: string;
+  name?: string;
+
+  /**
+   * Description of the schema.
+   */
+  description?: string;
 }
 
-export function ApiSchema(options: ApiSchemaOptions): ClassDecorator {
+export function ApiSchema(options?: ApiSchemaOptions): ClassDecorator {
   return createClassDecorator(DECORATORS.API_SCHEMA, [options]);
 }
diff --git a/lib/services/schema-object-factory.ts b/lib/services/schema-object-factory.ts
index df138154..9b352a38 100644
--- a/lib/services/schema-object-factory.ts
+++ b/lib/services/schema-object-factory.ts
@@ -246,6 +246,8 @@ export class SchemaObjectFactory {
     const extensionProperties =
       Reflect.getMetadata(DECORATORS.API_EXTENSION, type) || {};
 
+    const { schemaName, schemaProperties } = this.getSchemaMetadata(type);
+
     const typeDefinition: SchemaObject = {
       type: 'object',
       properties: mapValues(keyBy(propertiesWithType, 'name'), (property) => {
@@ -263,7 +265,8 @@ export class SchemaObjectFactory {
 
         return omit(property, [...keysToOmit, 'required']);
       }) as Record<string, SchemaObject | ReferenceObject>,
-      ...extensionProperties
+      ...extensionProperties,
+      ...schemaProperties
     };
 
     const typeDefinitionRequiredFields = propertiesWithType
@@ -277,23 +280,15 @@ export class SchemaObjectFactory {
     if (typeDefinitionRequiredFields.length > 0) {
       typeDefinition['required'] = typeDefinitionRequiredFields;
     }
-    const schemaName = this.getSchemaName(type);
     schemas[schemaName] = typeDefinition;
     return schemaName;
   }
 
-  getSchemaName(type: Function | Type<unknown>) {
-    const customSchema: ApiSchemaOptions[] = Reflect.getOwnMetadata(
-      DECORATORS.API_SCHEMA,
-      type
-    );
-
-    if (!customSchema || customSchema.length === 0) {
-      return type.name;
-    }
-
-    const schemaName = customSchema[customSchema.length - 1].name;
-    return schemaName ?? type.name;
+  getSchemaMetadata(type: Function | Type<unknown>) {
+    const schemas: ApiSchemaOptions[] =
+      Reflect.getOwnMetadata(DECORATORS.API_SCHEMA, type) ?? [];
+    const { name, ...schemaProperties } = schemas[schemas.length - 1] ?? {};
+    return { schemaName: name ?? type.name, schemaProperties };
   }
 
   mergePropertyWithMetadata(


Test patch:
diff --git a/e2e/api-spec.json b/e2e/api-spec.json
index 677c0b47..9ec24552 100644
--- a/e2e/api-spec.json
+++ b/e2e/api-spec.json
@@ -1409,6 +1409,7 @@
             "type": "number"
           }
         },
+        "description": "ExtraModel description",
         "required": [
           "one",
           "two"
diff --git a/e2e/src/cats/dto/extra-model.dto.ts b/e2e/src/cats/dto/extra-model.dto.ts
index c57491ee..66be96ce 100644
--- a/e2e/src/cats/dto/extra-model.dto.ts
+++ b/e2e/src/cats/dto/extra-model.dto.ts
@@ -1,7 +1,8 @@
 import { ApiProperty, ApiSchema } from '../../../../lib';
 
 @ApiSchema({
-  name: 'ExtraModel'
+  name: 'ExtraModel',
+  description: 'ExtraModel description'
 })
 export class ExtraModelDto {
   @ApiProperty()
diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts
index d09714b9..7db75e12 100644
--- a/test/services/schema-object-factory.spec.ts
+++ b/test/services/schema-object-factory.spec.ts
@@ -384,50 +384,109 @@ describe('SchemaObjectFactory', () => {
       });
     });
 
-    it('should use schema name instead of class name', () => {
-      @ApiSchema({
-        name: 'CreateUser'
-      })
-      class CreateUserDto {}
+    describe('@ApiSchema', () => {
+      it('should use the class name when no options object was passed', () => {
+        @ApiSchema()
+        class CreateUserDto {}
 
-      const schemas: Record<string, SchemasObject> = {};
+        const schemas: Record<string, SchemasObject> = {};
 
-      schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);
+        schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);
 
-      expect(Object.keys(schemas)).toContain('CreateUser');
-    });
+        expect(Object.keys(schemas)).toContain('CreateUserDto');
+      });
 
-    it('should not use schema name of base class', () => {
-      @ApiSchema({
-        name: 'CreateUser'
-      })
-      class CreateUserDto {}
+      it('should use the class name when the options object is empty', () => {
+        @ApiSchema({})
+        class CreateUserDto {}
 
-      class UpdateUserDto extends CreateUserDto {}
+        const schemas: Record<string, SchemasObject> = {};
 
-      const schemas: Record<string, SchemasObject> = {};
+        schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);
 
-      schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);
+        expect(Object.keys(schemas)).toContain('CreateUserDto');
+      });
 
-      expect(Object.keys(schemas)).toContain('UpdateUserDto');
-    });
+      it('should use the schema name instead of class name', () => {
+        @ApiSchema({
+          name: 'CreateUser'
+        })
+        class CreateUserDto {}
 
-    it('should override the schema name of base class', () => {
-      @ApiSchema({
-        name: 'CreateUser'
-      })
-      class CreateUserDto {}
+        const schemas: Record<string, SchemasObject> = {};
 
-      @ApiSchema({
-        name: 'UpdateUser'
-      })
-      class UpdateUserDto extends CreateUserDto {}
+        schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);
 
-      const schemas: Record<string, SchemasObject> = {};
+        expect(Object.keys(schemas)).toContain('CreateUser');
+      });
 
-      schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);
+      it('should not use the schema name of the base class', () => {
+        @ApiSchema({
+          name: 'CreateUser'
+        })
+        class CreateUserDto {}
+
+        class UpdateUserDto extends CreateUserDto {}
+
+        const schemas: Record<string, SchemasObject> = {};
+
+        schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);
+
+        expect(Object.keys(schemas)).toContain('UpdateUserDto');
+      });
 
-      expect(Object.keys(schemas)).toContain('UpdateUser');
+      it('should override the schema name of the base class', () => {
+        @ApiSchema({
+          name: 'CreateUser'
+        })
+        class CreateUserDto {}
+
+        @ApiSchema({
+          name: 'UpdateUser'
+        })
+        class UpdateUserDto extends CreateUserDto {}
+
+        const schemas: Record<string, SchemasObject> = {};
+
+        schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);
+
+        expect(Object.keys(schemas)).toContain('UpdateUser');
+      });
+
+      it('should use the the description if provided', () => {
+        @ApiSchema({
+          description: 'Represents a user.'
+        })
+        class CreateUserDto {}
+
+        const schemas: Record<string, SchemasObject> = {};
+
+        schemaObjectFactory.exploreModelSchema(CreateUserDto, schemas);
+
+        expect(schemas[CreateUserDto.name].description).toEqual(
+          'Represents a user.'
+        );
+      });
+
+      it('should not use the the description of the base class', () => {
+        @ApiSchema({
+          description: 'Represents a user.'
+        })
+        class CreateUserDto {}
+
+        @ApiSchema({
+          description: 'Represents a user update.'
+        })
+        class UpdateUserDto extends CreateUserDto {}
+
+        const schemas: Record<string, SchemasObject> = {};
+
+        schemaObjectFactory.exploreModelSchema(UpdateUserDto, schemas);
+
+        expect(schemas[UpdateUserDto.name].description).toEqual(
+          'Represents a user update.'
+        );
+      });
     });
 
     it('should include extension properties', () => {