routing-controllers-openapi icon indicating copy to clipboard operation
routing-controllers-openapi copied to clipboard

Swagger-UI fails to resolve reference when class passed as @ResponseSchema and @Body

Open rdfedor opened this issue 3 years ago • 11 comments

Have a project set up using the following structure,

./controllers/auth.ts
./validator/auth.ts
./main.ts

Here's a rough implementation of how the files are implemented,

./main.ts

import { routingControllersToSpec } from 'routing-controllers-openapi'
import { getMetadataArgsStorage } from 'routing-controllers'
import { validationMetadatasToSchemas } from 'class-validator-jsonschema'
import { defaultMetadataStorage } from 'class-transformer/cjs/storage'
import * as swaggerUi from 'swagger-ui-express'
import { useExpressServer } from 'routing-controllers'
import * as http from 'http'
import * as express from 'express'

const app = express()

const schemas = validationMetadatasToSchemas({
  classTransformerMetadataStorage: defaultMetadataStorage,
  refPointerPrefix: '#/components/schemas/',
})

app.use(
      '/api-docs',
      swaggerUi.serve,
      swaggerUi.setup(
        routingControllersToSpec(
          getMetadataArgsStorage(),
          {
            controllers: importClassesFromDirectories(
              routerControllerOptions.controllers,
            ),
          },
          {
            openapi: '3.0.0',
            schemas,
          },
        ),
      ),
    )

useExpressServer(app, {
  controllers: [`${__dirname}/controllers/**/*.ts`]
})

server.listen(8080, () => {
  console.log(`Server started on port 8080`)
})

./validator/auth.ts

import { IsNotEmpty, IsEmail, MaxLength } from 'class-validator'

export class AuthenticateUserRequest {
  @IsNotEmpty()
  @IsEmail()
  @MaxLength(254)
  public email: string

  @IsNotEmpty()
  public password: string
}

export class LoginToken {
  accessToken: string
  refreshToken: string
  maxAge?: string | number
  iat?: number
  roles?: string[]
  userUuid: string
  email: string
  firstName?: string
  lastName?: string
  verifiedEmail?: boolean
}

./controllers/auth.ts

import {
  JsonController,
  Post,
  ContentType,
  Body,
  ForbiddenError,
} from 'routing-controllers'
import { AuthenticateUserRequest, LoginToken } from '../validator/auth'

@JsonController('/api/auth')
export default class AuthController {
  @Post('/')
  @ContentType('application/json')
  @ResponseSchema(LoginToken)
  public async authenticateUser(
    @Body() req: AuthenticateUserRequest,
  ): Promise<LoginToken> {
    try {
      return await authService.processAuthenticateUserRequest(req)
    } catch (err) {
      if (
        err instanceof AccessDeniedError ||
        err instanceof NotFoundError ||
        (err instanceof Array &&
          err.filter(er => er instanceof ValidationError).length > 0)
      ) {
        throw new ForbiddenError('Invalid email and/or password.')
      }
      throw err
    }
  }
}

However when I try to expand the authenticateUser section of the request in swagger-ui I get the following error.

image

rdfedor avatar May 22 '21 03:05 rdfedor

I was able to fix part of the problem by moving the useExpressServer to before the app.use('/api-docs' line. It was able to get rid of the AuthenticateUserRequest error, but the LoginToken error still exists. Seems to only error out on classes that don't use class-validator.

rdfedor avatar May 22 '21 23:05 rdfedor

I am also getting schemas: {} even while using @ResponsesSchema

BlackRider97 avatar Jul 06 '21 13:07 BlackRider97

Same issue, workaround I use now is to annotate every field with an appropriate annotation e.g. IsNotEmpty() for strings. The dependencies we use are:

"dependencies": {
    "@google-cloud/firestore": "4.15.1",
    "bcrypt": "5.0.1",
    "body-parser": "1.19.0",
    "chalk": "4.1.2",
    "class-transformer": "^0.3.1",
    "class-validator": "^0.12.2",
    "class-validator-jsonschema": "2.2.0",
    "console-stamp": "3.0.3",
    "cors": "2.8.5",
    "express": "4.17.1",
    "express-jwt": "6.1.0",
    "jsonwebtoken": "8.5.1",
    "multer": "1.4.3",
    "routing-controllers": "0.9.0",
    "routing-controllers-openapi": "3.1.0",
    "swagger-ui-express": "4.1.3"
  },

judos avatar Dec 07 '21 16:12 judos

@judos workaround works, thanks

Any permanent solution planned on this?

thebrokenbar avatar Jan 24 '22 18:01 thebrokenbar

It's work for me also, I put in each string type of entity @IsNotEmpty() decoration , Thanks so much

diedhiouh avatar Nov 25 '22 10:11 diedhiouh

Same error :/

JamesDAdams avatar Jan 17 '24 17:01 JamesDAdams

solution : downgrave version : https://github.com/epiphone/routing-controllers-openapi/blob/master/sample/01-basic/package.json

JamesDAdams avatar Jan 17 '24 18:01 JamesDAdams

There is not well documented property type schemas

I got Screenshot from 2024-01-18 15-19-23

but then I just ignored it and all works well, final working variant

const { defaultMetadataStorage } = require('class-transformer/cjs/storage');
        const spec = routingControllersToSpec(getMetadataArgsStorage(), routingControllersOptions, {
            components: {
                // @ts-ignore
                schemas: validationMetadatasToSchemas({
                    classTransformerMetadataStorage: defaultMetadataStorage,
                    refPointerPrefix: '#/components/schemas/',
                })
            }
        });

deps

   "dependencies": {
      "bcrypt": "^5.1.1",
      "body-parser": "^1.20.2",
      "class-transformer": "^0.5.1",
      "class-validator": "^0.14.0",
      "class-validator-jsonschema": "^5.0.0",
      "compression": "^1.7.4",
      "event-dispatch": "^0.4.1",
      "express": "^4.18.2",
      "express-basic-auth": "^1.2.1",
      "joi": "^17.11.0",
      "jsonwebtoken": "^9.0.2",
      "morgan": "^1.10.0",
      "multer": "^1.4.5-lts.1",
      "reflect-metadata": "^0.1.14",
      "routing-controllers": "^0.10.4",
      "routing-controllers-openapi": "^4.0.0",
      "swagger-ui-express": "^5.0.0",
      "typedi": "^0.10.0",
      "winston": "^3.11.0"
   }

also, this lib relies on class-validator, so classes without this lib's annotation will be ignored

oleksandr-andrushchenko avatar Jan 18 '24 22:01 oleksandr-andrushchenko

@oleksandr-andrushchenko Could you please show your whole solution? For me, routingControllersToSpec() throws generateSpec.ts:341 )[index], TypeError: Cannot read properties of undefined (reading '0'), if i use @Body() of routing-controllers. All steps i made, what the documentation said. Im using Node 20, TypeScript, ESM, and a Koa2 server.

ernestyouniverse avatar Feb 03 '24 02:02 ernestyouniverse

@ernestyouniverse @all https://github.com/oleksandr-andrushchenko/ExamMeApi/blob/main/src/application.ts

if it helps - give me a star, thank you!

oleksandr-andrushchenko avatar Feb 05 '24 02:02 oleksandr-andrushchenko

@oleksandr-andrushchenko thank You for Your help! :) Indeed with a commonJS setup it works smoothly. Sadly for ESM it doesn't.

ernestyouniverse avatar Feb 12 '24 09:02 ernestyouniverse