Open API

This concern dynamically generates and serves an Open API v3 specification using swagger-core for all detected API endpoints.

Webservices

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.layouting.*
import org.genhttp.modules.openapi.*
import org.genhttp.modules.webservices.*

suspend fun main() {
    val api = Layout.create()
        .addService<UserService>("users")
        .addOpenApi()

    Host.create()
        .handler(api)
        .development()
        .run()
}

data class User(val id: Int, val name: String)

class UserService {

    @ResourceMethod
    fun getUsers(): List<User> = emptyList()

    @ResourceMethod(path = ":id")
    fun getUser(id: Int): User = User(id, "John Doe")

}

Functional

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.functional.*
import org.genhttp.modules.openapi.*

suspend fun main() {
    val api = Inline.create()
        .get("/users") { emptyList<User>() }
        .get("/user/:id") { id: Int -> User(id, "John Doe") }
        .addOpenApi()

    Host.create()
        .handler(api)
        .development()
        .run()
}

data class User(val id: Int, val name: String)

Controllers

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.controllers.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.openapi.*
import org.genhttp.modules.reflection.*

suspend fun main() {
    val api = Layout.create()
        .addController<UserController>("users")
        .addOpenApi()

    Host.create()
        .handler(api)
        .development()
        .run()
}

data class User(val id: Int, val name: String)

class UserController {

    @ControllerAction(Method.Get)
    fun list(): List<User> = emptyList()

    @ControllerAction(Method.Get)
    fun find(@FromPath id: Int): User = User(id, "John Doe")

}

This example provides an API with two operations and hosts a self-describing Open API specification via http://localhost:8080/openapi.json.

Automatically generated Open API v3 specification

You can import this specification into other tools such as Postman or Swagger Editor to execute requests or generate client implementations.

Generated specification loaded into Swagger Editor

Endpoints

The Open API concern provides the following endpoints:

EndpointDescription
/openapi.jsonOpen API specification serialized to application/json.
/openapi.yamlOpen API specification serialized to application/yaml.
/openapi.ymlOpen API specification serialized to application/yaml.
/openapiSerialized depending on the Accept header of the request, falls back to application/json.

Documentation

By default, the specification is derived from the signatures of the functions implementing the operations. To add descriptions, additional responses or headers, you can annotate your services, functions, parameters and models with the annotations provided by the org.genhttp.api.content.services package. As these annotations are part of the genhttp-api package, projects that only contain services or models do not need to depend on the Open API module.

Webservices

Kotlin
import org.genhttp.api.content.services.*
import org.genhttp.api.protocol.ResponseStatus
import org.genhttp.engine.internal.Host

import org.genhttp.modules.layouting.*
import org.genhttp.modules.openapi.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.webservices.*

suspend fun main() {
    val api = Layout.create()
        .addService<UserService>("users")
        .addOpenApi()

    Host.create()
        .handler(api)
        .development()
        .run()
}

@Summary("A user of the application")
data class User(
    @Summary("The unique identifier of the user") val id: Int,
    @Summary("The display name of the user") val name: String
)

data class ApiError(val message: String)

@Tag("Users")
@Summary("Manages the users of the application")
@ApiResponse(ResponseStatus.BadRequest, "The request could not be processed", type = ApiError::class)
class UserService {

    @ResourceMethod(path = ":id")
    @Summary("Fetches a single user")
    @ApiResponse(ResponseStatus.NotFound, "There is no user with the given ID")
    fun getUser(@Summary("The ID of the user to fetch") id: Int): @Summary("The user with the given ID") User =
        User(id, "John Doe")

    @ResourceMethod(Method.Post)
    @Summary("Creates a new user")
    @Remarks("The ID of the user is assigned by the server.")
    @ApiResponse(ResponseStatus.Created, "The newly created user")
    fun createUser(@Summary("The user to be created") user: User): Result<User> =
        Result(user).status(ResponseStatus.Created)

}

Functional

Kotlin
import org.genhttp.api.content.services.*
import org.genhttp.api.protocol.ResponseStatus
import org.genhttp.engine.internal.Host

import org.genhttp.modules.functional.*
import org.genhttp.modules.openapi.*
import org.genhttp.modules.reflection.*

suspend fun main() {
    val api = Inline.create()
        .get("/users/:id", ::getUser)
        .post("/users", ::createUser)
        .addOpenApi()

    Host.create()
        .handler(api)
        .development()
        .run()
}

@Summary("Fetches a single user")
@ApiResponse(ResponseStatus.NotFound, "There is no user with the given ID")
fun getUser(@Summary("The ID of the user to fetch") id: Int): @Summary("The user with the given ID") User =
    User(id, "John Doe")

@Summary("Creates a new user")
@Remarks("The ID of the user is assigned by the server.")
@ApiResponse(ResponseStatus.Created, "The newly created user")
fun createUser(@Summary("The user to be created") user: User): Result<User> =
    Result(user).status(ResponseStatus.Created)

@Summary("A user of the application")
data class User(
    @Summary("The unique identifier of the user") val id: Int,
    @Summary("The display name of the user") val name: String
)

Controllers

Kotlin
import org.genhttp.api.content.services.*
import org.genhttp.api.protocol.ResponseStatus
import org.genhttp.engine.internal.Host

import org.genhttp.modules.controllers.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.openapi.*
import org.genhttp.modules.reflection.*

suspend fun main() {
    val api = Layout.create()
        .addController<UserController>("users")
        .addOpenApi()

    Host.create()
        .handler(api)
        .development()
        .run()
}

@Summary("A user of the application")
data class User(
    @Summary("The unique identifier of the user") val id: Int,
    @Summary("The display name of the user") val name: String
)

data class ApiError(val message: String)

@Tag("Users")
@Summary("Manages the users of the application")
@ApiResponse(ResponseStatus.BadRequest, "The request could not be processed", type = ApiError::class)
class UserController {

    @ControllerAction(Method.Get)
    @Summary("Fetches a single user")
    @ApiResponse(ResponseStatus.NotFound, "There is no user with the given ID")
    fun find(@FromPath @Summary("The ID of the user to fetch") id: Int): @Summary("The user with the given ID") User =
        User(id, "John Doe")

    @ControllerAction(Method.Post)
    @Summary("Creates a new user")
    @Remarks("The ID of the user is assigned by the server.")
    @ApiResponse(ResponseStatus.Created, "The newly created user")
    fun create(@Summary("The user to be created") user: User): Result<User> =
        Result(user).status(ResponseStatus.Created)

}

The annotations are read from functions, and Kotlin does not allow annotations on the parameters of a lambda, so a functional handler that should be documented is implemented by a function and added as a function reference (::getUser), as shown above.

The following annotations are available:

AnnotationApplies toDescription
@SummaryServices, functions, parameters, return types, models, propertiesA short description of the annotated element.
@RemarksServices, functions, models, propertiesA detailed explanation, which will be rendered as Markdown by most tools.
@TagServices, functionsThe group the operations are listed under.
@ApiRequestBodyFunctionsDescribes the body expected by the operation.
@ApiResponseServices, functionsDeclares a response that may be generated by the operation.
@ApiRequestHeaderServices, functionsDeclares a header that is read from the request.
@ApiResponseHeaderServices, functionsDeclares a header that is sent with the response.

Annotations placed on a service or controller class apply to all operations provided by this class.

GenHTTP calls the annotations for bodies, responses and headers [RequestBody], [Response], [RequestHeader] and [ResponseHeader]. genhttp-kt prefixes them with Api (as known from Swagger), so they do not clash with the RequestBody, Response and RequestHeader types of the API. Annotation parameters cannot be null in Kotlin, so optional parameters that are not set are "" or Nothing::class, and the status of @ApiResponseHeader is an array with at most one entry.

Operations and Models

On a function, @Summary and @Remarks set the summary and the description of the operation. On a parameter, the summary describes the corresponding parameter of the operation (or the request body, if the argument is read from the body). On the return type (fun getUser(): @Summary("...") User), it describes the successful response of the operation.

On model types and their properties, @Summary and @Remarks are added to the generated JSON schema. For classes with a primary constructor, such as data classes, the annotations can be placed on the parameters of the constructor as shown in the example above, so there is no need to target the property with @property:Summary("...").

Functions annotated with @Deprecated (the one of Kotlin or of Java) will be flagged as deprecated.

Grouping

Operations are grouped by the name of the class they are declared in. Operations of functional handlers that are implemented by lambdas are grouped under Inline. Placing @Tag on a service class lists all of its operations under the given name, while summary and remarks of the class are used to describe this group. Placing @Tag on a function lists this operation under the given name, regardless of the class it is declared in.

A function reference added to a functional handler is grouped like a function of a service: under the name of the class or object declaring it. For a top-level function, this is the class the Kotlin compiler generates for its file (such as MainKt for Main.kt). Use @Tag on the function, or declare the functions in an object and add them as Users::getUser, to choose the group.

Request Bodies

The request body is derived from the parameters of the function. If your function reads the body on its own (e.g. from the request or a stream), or accepts specific content types only, you can describe the body with the @ApiRequestBody annotation. Declaring multiple bodies with different content types will merge them into a single request body. The annotation is ignored for GET operations.

Kotlin
import org.genhttp.api.content.services.ApiRequestBody
import org.genhttp.api.protocol.Request

import org.genhttp.modules.reflection.Method
import org.genhttp.modules.webservices.ResourceMethod

import java.io.InputStream

data class User(val id: Int, val name: String)

class UserService {

    @ResourceMethod(Method.Put, "avatar")
    @ApiRequestBody("The new avatar of the user", contentType = "image/png")
    @ApiRequestBody(contentType = "image/jpeg")
    fun setAvatar(content: InputStream) { /* ... */ }

    @ResourceMethod(Method.Post, "import")
    @ApiRequestBody("The users to be imported", type = Array<User>::class)
    suspend fun importUsers(request: Request) { /* ... */ }

}

If only a description is given, the content types and the schema derived from the function signature are kept. As annotations cannot carry type arguments, a list is described by the corresponding array type (Array<User>::class).

Responses

Responses are derived from the return type of the function. The @ApiResponse annotation declares additional responses, such as errors that may occur. If no description is given, it is derived from the status (e.g. Not Found), and the content type is derived from the serialization formats supported by the service, if a type is given. Use contentType to announce a specific format, e.g. for functions that return a dynamically generated Response:

Kotlin
import org.genhttp.api.content.services.ApiResponse
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.Response
import org.genhttp.api.protocol.ResponseStatus

import org.genhttp.modules.webservices.ResourceMethod

class UserService {

    @ResourceMethod(path = "avatar")
    @ApiResponse(ResponseStatus.Ok, "The avatar of the user", contentType = "image/png")
    @ApiResponse(ResponseStatus.Ok, contentType = "image/jpeg")
    fun getAvatar(request: Request): Response = TODO("render the avatar")

}

The following rules apply:

  • Declaring a successful response (2xx) on a function replaces the derived responses. This allows to announce that an operation responds with 201 Created instead of 200 OK. If no type is given, the return type of the function is used to describe the content.
  • Responses declared on a service class apply to all operations of this service that do not declare a response with the same status on their own. This allows to document common error responses in a single place.
  • Declaring multiple responses with the same status but different content types will merge them into a single response.

Headers

Headers read from the request or sent with the response can be declared with the @ApiRequestHeader and @ApiResponseHeader annotations. By default, headers are expected to be strings, which can be changed by setting the type parameter.

Kotlin
import org.genhttp.api.content.services.ApiRequestHeader
import org.genhttp.api.content.services.ApiResponse
import org.genhttp.api.content.services.ApiResponseHeader
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.ResponseStatus

import org.genhttp.modules.webservices.ResourceMethod

data class User(val id: Int, val name: String)

class UserService {

    @ResourceMethod
    @ApiRequestHeader("X-Page", "The page to be returned", type = Int::class)
    @ApiResponseHeader("X-Total-Count", "The total number of users", type = Int::class)
    @ApiResponse(ResponseStatus.TooManyRequests, "Too many requests have been sent")
    @ApiResponseHeader("Retry-After", "The number of seconds to wait", status = [ResponseStatus.TooManyRequests], type = Int::class)
    fun getUsers(request: Request): List<User> = TODO("read the requested page")

}

A request header is optional unless required is set to true. A response header without a status is added to all successful responses (2xx) of the operation. If a status is given, the header is only added if the operation declares a response with this status.

Concerns

Concerns such as range support add functionality to the content they wrap, e.g. by reading additional headers or generating additional responses. Concerns that implement the DocumentedConcern interface add this functionality to the generated specification. The following concerns shipped with the framework document themselves:

ConcernDocumentation
Client Caching (Policy)Expires header for successful GET responses
Client Caching (Validation)If-None-Match request header, 304 Not Modified response and ETag headers
DecompressionContent-Encoding request header for operations accepting a body
LocalizationContent-Language header for successful responses
Range SupportRange request header, 206 and 416 responses with the corresponding headers

To document a custom concern, implement DocumentedConcern instead of Concern. The addDocumentation function is called for every operation provided by the wrapped content, with method indicating the HTTP verb of the operation. Concerns may add ApiResponse, ApiRequestHeader and ApiResponseHeader instances (Kotlin allows to create annotations like any other object), other kinds of documentation are ignored.

Kotlin
import org.genhttp.api.content.Handler
import org.genhttp.api.content.services.ApiResponse
import org.genhttp.api.content.services.ApiResponseHeader
import org.genhttp.api.content.services.DocumentedConcern
import org.genhttp.api.content.services.OperationDocumentation
import org.genhttp.api.infrastructure.Server
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.Response
import org.genhttp.api.protocol.ResponseStatus

class RateLimitConcern(override val content: Handler) : DocumentedConcern {

    override suspend fun prepare(server: Server) = content.prepare(server)

    override suspend fun handle(request: Request): Response? {
        // check the rate limit and respond with 429 if exceeded
        return content.handle(request)
    }

    override fun addDocumentation(operation: OperationDocumentation) {
        operation.add(ApiResponse(ResponseStatus.TooManyRequests, "Too many requests have been sent"))
            .add(
                ApiResponseHeader(
                    "Retry-After",
                    "The number of seconds to wait",
                    status = arrayOf(ResponseStatus.TooManyRequests),
                    type = Int::class
                )
            )
    }

}

The documentation added by a concern is treated as if it was declared on the service class of the operation. Documentation declared on the service itself and documentation added by concerns closer to the content take precedence.

Customizing

To customize the generated specification you can pass a function that will be executed just before the document is serialized and sent to the requesting client. The function receives the io.swagger.v3.oas.models.OpenAPI model of swagger-core.

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.functional.*
import org.genhttp.modules.openapi.*

suspend fun main() {
    val description = ApiDescription.create()
        .title("My awesome API")
        .version("2.0.7")
        .postProcessor { _, document -> document.info.termsOfService = "https://mycompany.com/tos" }

    val api = Inline.create()
        .add(description)

    Host.create()
        .handler(api)
        .development()
        .run()
}

Without a title or version, the specification is titled Swagger specification with version 1.0.0.

Caching

As automatically generating a specification is an expensive task, the concern will cache the generated document for improved performance. In some cases you might want the specification to be generated on every request, especially if the result depends on the request sent by the client. In this case, you can disable caching, causing the document to be generated on every request.

Kotlin
val description = ApiDescription.create()
    .caching(false)

Visualizing Definitions

To explore your APIs using graphical applications such as Swagger or Redoc, you can use the API browsing functionality.

Custom Discovery

By default, automatic discovery will handle functional handlers, webservices and controllers and will traverse automatically through layouts and concerns. If you would like to support an additional handler, you can initialize a registry and pass a custom ApiExplorer.

The following example adds support for redirects, so the generated Open API specification will describe the operation.

Kotlin
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.Operation
import io.swagger.v3.oas.models.PathItem
import io.swagger.v3.oas.models.headers.Header
import io.swagger.v3.oas.models.media.StringSchema
import io.swagger.v3.oas.models.responses.ApiResponse
import io.swagger.v3.oas.models.responses.ApiResponses

import org.genhttp.api.content.Handler
import org.genhttp.api.protocol.Request
import org.genhttp.engine.internal.Host

import org.genhttp.modules.layouting.*
import org.genhttp.modules.openapi.*
import org.genhttp.modules.openapi.discovery.*
import org.genhttp.modules.redirects.*
import org.genhttp.modules.redirects.provider.RedirectProvider

suspend fun main() {
    val discovery = ApiDiscovery.default()
        .add<RedirectExplorer>()

    val description = ApiDescription.with(discovery)

    val api = Layout.create()
        .add("google", Redirect.to("https://google.com"))
        .add(description)

    Host.create()
        .handler(api)
        .development()
        .run()
}

class RedirectExplorer : ApiExplorer {

    override fun canExplore(handler: Handler): Boolean = handler is RedirectProvider

    override suspend fun explore(
        request: Request,
        handler: Handler,
        path: List<String>,
        document: OpenAPI,
        schemata: SchemaManager,
        documentation: InheritedDocumentation,
        registry: ApiDiscoveryRegistry
    ) {
        if (handler is RedirectProvider) {
            val apiPath = buildPath("", path, false)

            val location = Header()
                .schema(StringSchema())
                .example(handler.target.toString())

            val response = ApiResponse()
                .description(if (handler.temporary) "Temporary Redirect" else "Moved Permanently")
                .addHeaderObject("Location", location)

            val status = if (handler.temporary) "307" else "301"

            val operation = Operation()
                .responses(ApiResponses().addApiResponse(status, response))

            document.path(apiPath, PathItem().get(operation))
        }
    }

}

Explorers for handlers that wrap other handlers should pass the given documentation on when exploring their children via the registry, so that the documentation added by concerns further up in the chain is applied to the operations found.

If you would like to have an API explorer for a handler that is shipped with the framework, feel free to get in touch via a GitHub issue.