Search code examples
nestjsnestjs-swagger

NestJs/swagger: Define ref schemas without DTO classes


I have an app where I define the API response schemas as plain javascript objects according to the open-api spec. Currently I am passing that to the ApiResponse decorator in @nestjs/swagger as follows:

class CatsController {

  @Get()
  @ApiResponse({
    status: 200,
    schema: catSchema // plain js object imported from another file
  })
  getAll() {}
}

This is working great. However, the output open-api spec contains the verbose schema for every endpoint which uses the catSchema. Instead, I want the output swagger file to have the catSchema under the components section, and have a corresponding $ref in the paths section.

components:
  schemas:
    Cat:
      properties:
        name:
          type: string
paths:
  /cats/{id}:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cat'

So far, it seems the only way to do that would be to define the schema as a DTO class and use the ApiProperty decorator for each class property. In my case, that means I have to refactor all the plain object schemas in open-api spec to be DTO classes.

Is there a way to feed the raw schema to the library and get the expected outcome?

// instead of this:
class CatDto {
  @ApiProperty()
  name: string;
}

// I want to do:
const catSchema = {
  type: 'object',
  properties: {
    name: { type: 'string' }
  }
}

Solution

  • After days and days of trial and error, I was able to pull this off using an interesting trick in Javascript.

    First I created the open-api spec as a plain object (as asked in the question). Then passed that to a new decorator, where the magic happens.

    In the decorator, I create a DTO class with a predefined name, and map the properties from the plain object to the DTO class. The tricky part is to give it a name dynamically. That can be achieved by the following technique.

    const dynamicName = 'foo'; // passed as a parameter to the decorator
    
    class IntermediateDTO {
      @ApiProperty(schema) // schema as a plain object
      data: any;
    }
    
    const proxyObject = {
      [dynamicName] = class extends IntermediateDTO {}
    }
    

    By using the proxy object, and assigning class extends IntermediateDTO {} to a property in that, the entry gets a name dynamically. Now this new DTO with the dynamic name can be passed to the ApiResponse decorator of @nestjs/swagger to achieve the expected result.