@rnwonder/adonis-autoswagger
v1.1.5
Published
Auto-Generate swagger docs for AdonisJS
Readme
Auto-Generate swagger docs for AdonisJS
� Crediats
This package is based on the excellent work by ad-on-is in the original adonis-autoswagger project. This fork adds enhanced relation field selection capabilities, improved HMR support and other features while maintaining compatibility with the original API.
💻️ Install
pnpm i @rnwonder/adonis-autoswagger #using pnpm⭐️ Features
- Creates paths automatically based on
routes.ts - Creates schemas automatically based on
app/Models/* - Creates schemas automatically based on
app/Interfaces/* - Creates schemas automatically based on
app/Validators/*(only for adonisJS v6) - Creates schemas automatically based on
app/Types/*(only for adonisJS v6) - Rich configuration via comments
- Null value support - Handle nullable fields with proper OpenAPI schemas
- Multiple response bodies - Support multiple response variations for the same status code using OpenAPI
oneOf - Works also in production mode
node ace docs:generatecommand
✌️Usage
Create a file /config/swagger.ts
// for AdonisJS v6
import path from "node:path";
import url from "node:url";
// ---
export default {
// path: __dirname + "/../", for AdonisJS v5
path: path.dirname(url.fileURLToPath(import.meta.url)) + "/../", // for AdonisJS v6
title: "Foo", // use info instead
version: "1.0.0", // use info instead
description: "", // use info instead
tagIndex: 2,
productionEnv: "production", // optional
info: {
title: "title",
version: "1.0.0",
description: "",
},
snakeCase: true,
debug: false, // set to true, to get some useful debug output
ignore: ["/swagger", "/docs"],
preferredPutPatch: "PUT", // if PUT/PATCH are provided for the same route, prefer PUT
common: {
parameters: {}, // OpenAPI conform parameters that are commonly used
headers: {}, // OpenAPI conform headers that are commonly used
},
securitySchemes: {}, // optional
authMiddlewares: ["auth", "auth:api"], // optional
defaultSecurityScheme: "BearerAuth", // optional
persistAuthorization: true, // persist authorization between reloads on the swagger page
showFullPath: false, // the path displayed after endpoint summary
ignoreDefaultPatterns: false, // ignore default patterns such as /uploads/* /assets/* /static/* /public/*
};In your routes.ts
6️⃣ for AdonisJS v6
import AutoSwagger from "adonis-autoswagger";
import swagger from "#config/swagger";
// returns swagger in YAML
router.get("/swagger", async () => {
return AutoSwagger.default.docs(router.toJSON(), swagger);
});
// Renders Swagger-UI and passes YAML-output of /swagger
router.get("/docs", async () => {
return AutoSwagger.default.ui("/swagger", swagger);
// return AutoSwagger.default.scalar("/swagger"); to use Scalar instead. If you want, you can pass proxy url as second argument here.
// return AutoSwagger.default.rapidoc("/swagger", "view"); to use RapiDoc instead (pass "view" default, or "read" to change the render-style)
});5️⃣ for AdonisJS v5
import AutoSwagger from "adonis-autoswagger";
import swagger from "Config/swagger";
// returns swagger in YAML
Route.get("/swagger", async () => {
return AutoSwagger.docs(Route.toJSON(), swagger);
});
// Renders Swagger-UI and passes YAML-output of /swagger
Route.get("/docs", async () => {
return AutoSwagger.ui("/swagger", swagger);
});👍️ Done
Visit http://localhost:3333/docs to see AutoSwagger in action.
HMR (Hot Module Reloading) Support
If you're using node ace serve --hmr, validators might not appear in your Swagger documentation due to dynamic import limitations. To fix this, register your validators during app startup:
Step 1: Register Validators
In your app startup file (e.g., start/kernel.ts or similar), register your validators:
import { registerValidators } from "@rnwonder/adonis-autoswagger";
import AddressValidator from "#validators/address_validator";
import UserValidator from "#validators/user_validator";
import OrderValidator from "#validators/order_validator";
// Register all validators for HMR compatibility
registerValidators({
AddressValidator,
UserValidator,
OrderValidator,
});Step 2: Use Validators in Documentation
Now you can use your validators in your controller annotations as usual:
/**
* @requestBody <AddressValidator>
* @responseBody 200 - Address created successfully
*/
async create({ request }: HttpContextContract) {
// Your controller logic
}Alternative: Use --watch instead of --hmr
If you prefer not to register validators manually, you can use:
node ace serve --watchinstead of --hmr. This mode supports automatic validator discovery.
💡 Compatibility
For controllers to get detected properly, please load them lazily.
✅ const TestController = () => import('#controllers/test_controller')
❌ import TestController from '#controllers/test_controller'🧑💻 Advanced usage
Additional configuration
info See Swagger API General Info for details.
securitySchemes
Add/Overwrite security schemes Swagger Authentication for details.
// example to override ApiKeyAuth
securitySchemes: {
ApiKeyAuth: {
type: "apiKey"
in: "header",
name: "X-API-Key"
}
}defaultSecurityScheme
Override the default security scheme.
- BearerAuth
- BasicAuth
- ApiKeyAuth
- your own defined under
securitySchemes
authMiddlewares
If a route uses a middleware named auth, auth:api, AutoSwagger will detect it as a Swagger security method. However, you can implement other middlewares that handle authentication.
Modify generated output
Route.get("/myswagger", async () => {
const json = await AutoSwagger.json(Route.toJSON(), swagger);
// modify json to your hearts content
return AutoSwagger.jsonToYaml(json);
});
Route.get("/docs", async () => {
return AutoSwagger.ui("/myswagger", swagger);
});Custom Paths in adonisJS v6
AutoSwagger supports the paths set in package.json. Interfaces are expected to be in app/interfaces, also all types and enums should be under app/types. However, you can override this, by modifying package.json as follows.
//...
"imports": {
// ...
"#interfaces/*": "./app/custom/path/interfaces/*.js",
"#types/*": "./app/custom/path/types/*.js"
// ...
}
//...
📃 Configure
tagIndex
Tags endpoints automatically
- If your routes are
/api/v1/products/...then your tagIndex should be3 - If your routes are
/v1/products/...then your tagIndex should be2 - If your routes are
/products/...then your tagIndex should be1
ignore
Ignores specified paths. When used with a wildcard (*), AutoSwagger will ignore everything matching before/after the wildcard.
/test/_will ignore everything starting with/test/, whereas \*/testwill ignore everything ending with/test.
common
Sometimes you want to use specific parameters or headers on multiple responses.
Example: Some resources use the same filter parameters or return the same headers.
Here's where you can set these and use them with @paramUse() and @responseHeader() @use(). See practical example for further details.
Null Value Support
AutoSwagger now properly handles null values in JSON response bodies. You can document APIs that return either objects or null without parsing errors.
Example
/**
* @description Get user information
* @responseBody 200 - {"user": null, "message": "User not found"}
* @responseBody 200 - {"user": "<User>", "message": "User found"}
*/
public async getUser({ response }: HttpContextContract) {
// Your logic here
}This generates proper OpenAPI schemas with nullable: true for null values, following OpenAPI 3.0 specifications.
Multiple Response Bodies
You can now document multiple response variations for the same HTTP status code. AutoSwagger automatically combines them using OpenAPI's oneOf schema with individual examples.
Example
/**
* @description User authentication endpoint
* @responseBody 200 - {"user": "<User>", "message": "Login successful", "token": "string"}
* @responseBody 200 - {"user": null, "message": "Invalid credentials", "token": null}
* @responseBody 429 - {"error": "Too many requests", "retryAfter": "number"}
*/
public async login({ request, response }: HttpContextContract) {
// Your authentication logic here
}Benefits
- Clear API Documentation: Shows all possible response variations
- Better Developer Experience: API consumers understand all possible responses
- OpenAPI 3.0 Compliance: Uses standard
oneOffor multiple schemas - Individual Examples: Each response variation has its own example
- Backward Compatibility: Single response bodies work exactly as before
💫 Extend Controllers
Add additional documentation to your Controller-files
@summary (only one) A summary of what the action does
@tag (only one) Set a custom tag for this action
@description (only one) A detailed description of what the action does.
@operationId (only one) An optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API..
@responseBody (multiple)
Format: <status> - <return> - <description>
<return> can be either a <Schema>, <Schema[]>/ or a custom JSON {}
@responseHeader (multiple)
Format: <status> - <name> - <description> - <meta>
@paramType (multiple)
Type can be one of Parameter Types (first letter in uppercase)
@requestBody (only one) A definition of the expected requestBody
Format: <body>
<body> can be either a <Schema>, <Schema[]>/, or a custom JSON {}
@requestFormDataBody (only one) A definition of the expected requestBody that will be sent with formData format.
Schema
A model or a validator.
Format: <Schema>
Custom format
Format: {"fieldname": {"type":"string", "format": "email"}}
This format should be a valid openapi 3.x json.
🤘Examples
@responseBody examples
@responseBody <status> - Lorem ipsum Dolor sit amet
@responseBody <status> // returns standard <status> message
@responseBody <status> - <Model> // returns model specification
@responseBody <status> - <Model[]> // returns model-array specification
@responseBody <status> - <Model>.with(relations, property1, property2.relations, property3.subproperty.relations) // returns a model and a defined relation
@responseBody <status> - <Model[]>.with(relations).exclude(property1, property2, property3.subproperty) // returns model specification
@responseBody <status> - <Model[]>.append("some":"valid json") // append additional properties to a Model
@responseBody <status> - <Model[]>.paginated() // helper function to return adonisJS conform structure like {"data": [], "meta": {}}
@responseBody <status> - <Model[]>.paginated(dataName, metaName) // returns a paginated model with custom keys for the data array and meta object, use `.paginated(dataName)` or `.paginated(,metaName)` if you want to override only one. Don't forget the ',' for the second parameter.
@responseBody <status> - <Model>.only(property1, property2) // pick only specific properties
@responseBody <status> - <Model[]>.with(relations).only(relation.field1, relation.field2) // include relations but only show specific fields from each relation
@requestBody <status> <myCustomValidator> // returns a validator object
@responseBody <status> - {"foo": "bar", "baz": "<Model>"} //returns custom json object and also parses the model
@responseBody <status> - ["foo", "bar"] //returns custom json array
// 🆕 NEW: Null value support
@responseBody <status> - {"user": null, "message": "Not found"} // properly handles null values
// 🆕 NEW: Multiple response bodies for same status code
@responseBody 200 - {"user": "<User>", "status": "success"} // success case
@responseBody 200 - {"user": null, "status": "not_found"} // not found case
// Both responses above will be combined using OpenAPI oneOf with separate examplesAdvanced Relation Field Selection
You can now combine .with() and .only() to include specific relations while selecting only certain fields from each relation. This is particularly useful for optimizing API responses by including only the data you need.
Basic Syntax
@responseBody <status> - <Model[]>.with(relation1, relation2.nestedRelation).only(field1, field2, relation1.field1, relation2.nestedRelation.field1)How it works
.with()specifies which relations to include.only()specifies which fields to show from the model and its relations- Base model fields are always included unless explicitly filtered
- Relations are only included if specified in
.with()or have nested field specifications in.only() - Relation fields are filtered based on the
.only()specifications
Examples
Example 1: Basic relation field selection
/**
* @responseBody 200 - <User[]>.with(profile, posts).only(id, name, profile.bio, profile.avatar, posts.title, posts.createdAt)
*/Result: Users with only id and name, profile with only bio and avatar, posts with only title and createdAt.
Example 2: Nested relation field selection
/**
* @responseBody 200 - <Order[]>.with(buyer, seller.user, products).only(id, status, buyer.firstName, buyer.lastName, seller.businessName, seller.user.email, products.name, products.price)
*/Result: Orders with basic fields, buyer names, seller business name and user email, product name and price.
Example 3: Deep nested relations
/**
* @responseBody 200 - <Post[]>.with(author.profile, comments.author).only(title, content, author.name, author.profile.bio, comments.text, comments.author.name)
*/Result: Posts with title/content, author name and bio, comments with text and author names.
Example 4: Mixed field selection (your working example)
/**
* @responseBody 200 - <HeadsUp[]>.with(funder, rotorParticipant.subscription, rotorParticipant.rotor, groupMember.group.creator, order.products, order.buyer, order.seller.user).only(funder, rotorParticipant.id, rotorParticipant.subscription.id, rotorParticipant.subscription.nextPaymentDate, rotorParticipant.rotor.name, groupMember.group.creator.firstName, order.products.name, order.buyer.firstName, order.seller.user.email).paginated()
*/Result: HeadsUps with all base fields, complete funder object, selected fields from nested relations.
Important Notes
- Base model fields: Always included unless you want to restrict them (rare use case)
- Relation inclusion: Relations must be specified in
.with()to be included - Field filtering: Use
.only()to specify exactly which fields you want from each relation - Nested paths: Use dot notation like
relation.nestedRelation.fieldfor deep nesting - Complete relations: If a relation is in
.with()but has no.only()specifications, all its fields are included - Missing fields: If you specify a field that doesn't exist in the schema, it will be ignored
Field Selection Rules
- Include all relation fields:
.with(relation)(no.only()for that relation) - Include specific relation fields:
.with(relation).only(relation.field1, relation.field2) - Include nested relation fields:
.with(relation.nested).only(relation.nested.field1) - Include complete nested relation:
.with(relation.nested)(no.only()for nested relation)
## `@paramPath` and `@paramQuery` examples
```ts
// basicaly same as @response, just without a status
@paramPath <paramName> - Description - (meta)
@paramQuery <paramName> - Description - (meta)
@paramPath id - The ID of the source - @type(number) @required
@paramPath slug - The ID of the source - @type(string)
@paramQuery q - Search term - @type(string) @required
@paramQuery page - the Page number - @type(number)
@requestBody examples
// basicaly same as @response, just without a status
@requestBody <Model> // Expects model specification
@requestBody <myCustomValidator> // Expects validator specification
@requestBody <Model>.with(relations) // Expects model and its relations
@requestBody <Model[]>.append("some":"valid json") // append additional properties to a Model
@requestBody {"foo": "bar"} // Expects a specific JSON@requestFormDataBody examples
// Providing a raw JSON
@requestFormDataBody {"name":{"type":"string"},"picture":{"type":"string","format":"binary"}} // Expects a valid OpenAPI 3.x JSON// Providing a Model, and adding additional fields
@requestFormDataBody <Model> // Expects a valid OpenAPI 3.x JSON
@requestFormDataBody <Model>.exclude(property1).append("picture":{"type":"string","format":"binary"}) // Expects a valid OpenAPI 3.x JSONPractical example
config/swagger.ts
export default {
path: __dirname + "../",
title: "YourProject",
version: "1.0.0",
tagIndex: 2,
ignore: ["/swagger", "/docs", "/v1", "/", "/something/*", "*/something"],
common: {
parameters: {
sortable: [
{
in: "query",
name: "sortBy",
schema: { type: "string", example: "foo" },
},
{
in: "query",
name: "sortType",
schema: { type: "string", example: "ASC" },
},
],
},
headers: {
paginated: {
"X-Total-Pages": {
description: "Total amount of pages",
schema: { type: "integer", example: 5 },
},
"X-Total": {
description: "Total amount of results",
schema: { type: "integer", example: 100 },
},
"X-Per-Page": {
description: "Results per page",
schema: { type: "integer", example: 20 },
},
},
},
},
};app/Controllers/Http/SomeController.ts
export default class SomeController {
/**
* @index
* @operationId getProducts
* @description Returns array of producs and it's relations
* @responseBody 200 - <Product[]>.with(relations)
* @paramUse(sortable, filterable)
* @responseHeader 200 - @use(paginated)
* @responseHeader 200 - X-pages - A description of the header - @example(test)
*/
public async index({ request, response }: HttpContextContract) {}
/**
* @show
* @paramPath id - Describe the path param - @type(string) @required
* @paramQuery foo - Describe the query param - @type(string) @required
* @description Returns a product with it's relation on user and user relations
* @responseBody 200 - <Product>.with(user, user.relations)
* @responseBody 404
*/
public async show({ request, response }: HttpContextContract) {}
/**
* @update
* @responseBody 200
* @responseBody 404 - Product could not be found
* @requestBody <Product>
*/
public async update({ request, response }: HttpContextContract) {}
/**
* @myCustomFunction
* @summary Lorem ipsum dolor sit amet
* @paramPath provider - The login provider to be used - @enum(google, facebook, apple)
* @responseBody 200 - {"token": "xxxxxxx"}
* @requestBody {"code": "xxxxxx"}
*/
public async myCustomFunction({ request, response }: HttpContextContract) {}
/**
* 🆕 NEW FEATURES SHOWCASE
* @login
* @summary User authentication with multiple response scenarios
* @description Authenticate user with email and password, demonstrating null value support and multiple response bodies
* @responseBody 200 - {"user": "<User>", "token": "string", "message": "Login successful"} - Successful login
* @responseBody 200 - {"user": null, "token": null, "message": "Invalid credentials"} - Failed authentication
* @responseBody 200 - {"user": null, "token": null, "message": "Account not activated"} - Inactive account
* @responseBody 429 - {"error": "Too many attempts", "retryAfter": 300}
* @requestBody {"email": "string", "password": "string"}
*/
public async login({ request, response }: HttpContextContract) {
// Multiple response variations will be automatically combined using OpenAPI oneOf
// Each response will have its own example in the documentation
}
/**
* @getUserProfile
* @summary Get user profile with nullable relations
* @description Demonstrates null value handling in complex nested objects
* @responseBody 200 - {"user": "<User>", "profile": {"bio": "string", "avatar": null}, "preferences": null}
* @responseBody 404 - {"user": null, "profile": null, "preferences": null, "error": "User not found"}
*/
public async getUserProfile({ request, response }: HttpContextContract) {
// Null values in nested objects are now properly handled
}
}What does it do?
AutoSwagger tries to extracat as much information as possible to generate swagger-docs for you.
Paths
Automatically generates swagger path-descriptions, based on your application routes. It also detects endpoints, protected by the auth-middlware.

Responses and RequestBody
Generates responses and requestBody based on your simple Controller-Annotation (see Examples)
Schemas
Models
Automatically generates swagger schema-descriptions based on your models

Interfaces
Instead of using param: any you can now use custom interfaces param: UserDetails. The interfaces files need to be located at app/Interfaces/
Enums
If you use enums in your models, AutoSwagger will detect them from app/Types/ folder and add them to the schema.
If you want to add enum on ExampleValue, you can use .append(enumFieldExample)
Example:
@responseBody 200 - <Model>.with(relations).append(enumFieldExample)Extend Models
Add additional documentation to your Models properties.
SoftDelete
Either use compose(BaseModel, SoftDeletes) or add a line @swagger-softdeletes to your Model.
Attention
The below comments MUST be placed 1 line above the property.
@no-swagger
Although, autoswagger detects serializeAs: null fields automatically, and does not show them. You can use @no-swagger for other fields.
@enum(foo, bar) If a field has defined values, you can add them into an enum. This is usesfull for something like a status field.
@format(string) Specify a format for that field, i.e. uuid, email, binary, etc...
@example(foo bar) Use this field to provide own example values for specific fields
@props({"minLength": 10, "foo": "bar"}) Use this field to provide additional properties to a field, like minLength, maxLength, etc. Needs to bee valid JSON.
@required Specify that the field is required
// SomeModel.js
@hasMany(() => ProductView)
// @no-swagger
public views: HasMany<typeof ProductView>
@column()
// @enum(pending, active, deleted)
public status: string
@column()
// @example([email protected])
public email: string
@column()
// @props({"minLength": 10})
public age: number
Production environment
[!WARNING] Make sure NODE_ENV=production in your production environment or whatever you set in
options.productionEnv
To make it work in production environments, additional steps are required
Create a new command for
docs:generateSee official documentation- This should create a new file in
commands/DocsGenerate.ts
- This should create a new file in
Use the provided
DocsGenerate.ts.examle/DocsGeneratev6.ts.exampleand put its contents into your newly createdDocsGenerate.tsModify
/start/env.tsas follows
//...
// this is necessary to make sure that the `DocsGenerate` command will run in CI/CD pipelines without setting environment variables
const isNodeAce = process.argv.some(
(arg) => arg.endsWith("/ace") || arg === "ace"
);
export default await Env.create(
new URL("../", import.meta.url),
isNodeAce
? {}
: {
// leave other settings as is
NODE_ENV: Env.schema.enum([
"development",
"production",
"test",
] as const),
PORT: Env.schema.number(),
}
);
//...- Execute the following
node ace docs:generate
node ace build --production
cp swagger.yml build/Known Issues
- Interfaces with objects are not working like
interface Test {foo: {bar: string}}- Solution, just extract the object as it's own interface
- Models with Union types like
status: "paid" | "not paid"will throw error- Solution, just extract the Union as its own type
