routing-controllers-openapi
routing-controllers-openapi copied to clipboard
Swagger-UI fails to resolve reference when class passed as @ResponseSchema and @Body
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.
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.
I am also getting schemas: {}
even while using @ResponsesSchema
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 workaround works, thanks
Any permanent solution planned on this?
It's work for me also, I put in each string type of entity @IsNotEmpty()
decoration , Thanks so much
Same error :/
solution : downgrave version : https://github.com/epiphone/routing-controllers-openapi/blob/master/sample/01-basic/package.json
There is not well documented property type schemas
I got
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 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 @all https://github.com/oleksandr-andrushchenko/ExamMeApi/blob/main/src/application.ts
if it helps - give me a star, thank you!
@oleksandr-andrushchenko thank You for Your help! :) Indeed with a commonJS setup it works smoothly. Sadly for ESM it doesn't.