Generating OpenAPI documentation

To use, add the following dependencies:

"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "0.18.0-M6"
"com.softwaremill.sttp.tapir" %% "tapir-openapi-circe-yaml" % "0.18.0-M6"

Tapir contains a case class-based model of the openapi data structures in the openapi/openapi-model subproject (the model is independent from all other tapir modules and can be used stand-alone).

An endpoint can be converted to an instance of the model by importing the sttp.tapir.docs.openapi.OpenAPIDocsInterpreter object:

import sttp.tapir._
import sttp.tapir.openapi.OpenAPI
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter

val booksListing = endpoint.in(path[String]("bookId"))

val docs: OpenAPI = OpenAPIDocsInterpreter.toOpenAPI(booksListing, "My Bookshop", "1.0")

Such a model can then be refined, by adding details which are not auto-generated. Working with a deeply nested case class structure such as the OpenAPI one can be made easier by using a lens library, e.g. Quicklens.

The documentation is generated in a large part basing on schemas. Schemas can be automatically derived and customised.

Quite often, you’ll need to define the servers, through which the API can be reached. To do this, you can modify the returned OpenAPI case class either directly or by using a helper method:

import sttp.tapir.openapi.Server

val docsWithServers: OpenAPI = OpenAPIDocsInterpreter.toOpenAPI(booksListing, "My Bookshop", "1.0")
  .servers(List(Server("https://api.example.com/v1").description("Production server")))

Multiple endpoints can be converted to an OpenAPI instance by calling the method on a list of endpoints:

OpenAPIDocsInterpreter.toOpenAPI(List(addBook, booksListing, booksListingByGenre), "My Bookshop", "1.0")

The openapi case classes can then be serialised to YAML using Circe:

import sttp.tapir.openapi.circe.yaml._

println(docs.toYaml)

Or to JSON:

import io.circe.Printer
import io.circe.syntax._
import sttp.tapir.openapi.circe._

println(Printer.spaces2.print(docs.asJson))

Options

Options can be customised by providing an implicit instance of OpenAPIDocsOptions, when calling .toOpenAPI.

  • operationIdGenerator: each endpoint corresponds to an operation in the OpenAPI format and should have a unique operation id. By default, the name of endpoint is used as the operation id, and if this is not available, the operation id is auto-generated by concatenating (using camel-case) the request method and path.
  • referenceEnums: defines if enums should be converted to open api components and referenced later. This option can be applied to all enums in the schema, or only specific ones. SObjectInfo input parameter is a unique identifier of object in the schema. By default, it is fully qualified name of the class (when using Validator.derivedEnum or implicits from sttp.tapir.codec.enumeratum._).

OpenAPI Specification Extensions

It’s possible to extend specification with extensions. There are .docsExtension methods available on Input/Output parameters and on endpoint:

case class MyExtension(string: String, int: Int)

val sampleEndpoint =
  endpoint.post
    .in("path-hello" / path[String]("world").extension("x-path", 22))
    .in(query[String]("hi").docsExtension("x-query", 33))
    .in(jsonBody[FruitAmount].docsExtension("x-request", MyExtension("a", 1)))
    .out(jsonBody[FruitAmount].docsExtension("x-response", List("array-0", "array-1")).docsExtension("x-response", "foo"))
    .errorOut(stringBody.docsExtension("x-error", "error-extension"))
    .docsExtension("x-endpoint-level-string", "world")
    .docsExtension("x-endpoint-level-int", 11)
    .docsExtension("x-endpoint-obj", MyExtension("42.42", 42))

val rootExtensions = List(
  DocsExtension.of("x-root-bool", true),
  DocsExtension.of("x-root-list", List(1, 2, 4))
)

val openAPIYaml = OpenAPIDocsInterpreter.toOpenAPI(sampleEndpoint, Info("title", "1.0"), rootExtensions).toYaml

However, to add extensions to other unusual places (like, License or Server, etc.) you should modify the OpenAPI object manually or using f.e. Quicklens

Exposing OpenAPI documentation

Exposing the OpenAPI documentation can be very application-specific. However, tapir contains modules which contain akka-http/http4s routes for exposing documentation using Swagger UI or Redoc:

// Akka HTTP
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-akka-http" % "0.18.0-M6"
"com.softwaremill.sttp.tapir" %% "tapir-redoc-akka-http" % "0.18.0-M6"

// Finatra
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-finatra" % "0.18.0-M6"

// HTTP4S
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-http4s" % "0.18.0-M6"
"com.softwaremill.sttp.tapir" %% "tapir-redoc-http4s" % "0.18.0-M6"

// Play
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-play" % "0.18.0-M6"
"com.softwaremill.sttp.tapir" %% "tapir-redoc-play" % "0.18.0-M6"

// Vert.x
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-vertx" % "0.18.0-M6"

Note: tapir-swagger-ui-akka-http transitively pulls some Akka modules in version 2.6. If you want to force your own Akka version (for example 2.5), use sbt exclusion. Mind the Scala version in artifact name:

"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-akka-http" % "0.18.0-M6" exclude("com.typesafe.akka", "akka-stream_2.12")

Usage example for akka-http:

import sttp.tapir._
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.tapir.openapi.circe.yaml._
import sttp.tapir.swagger.akkahttp.SwaggerAkka

val myEndpoints: Seq[Endpoint[_, _, _, _]] = ???
val docsAsYaml: String = OpenAPIDocsInterpreter.toOpenAPI(myEndpoints, "My App", "1.0").toYaml
// add to your akka routes
new SwaggerAkka(docsAsYaml).routes

For redoc, use RedocAkkaHttp.

For http4s, use the SwaggerHttp4s or RedocHttp4s classes.

For Play, use SwaggerPlay or RedocPlay classes.

For Vert.x, use SwaggerVertx class.

Using with sbt-assembly

The tapir-swagger-ui-* modules rely on a file in the META-INF directory tree, to determine the version of the Swagger UI. You need to take additional measures if you package your application with sbt-assembly because the default merge strategy of the assembly task discards most artifacts in that directory. To avoid a NullPointerException, you need to include the following file explicitly:

assemblyMergeStrategy in assembly := {
  case PathList("META-INF", "maven", "org.webjars", "swagger-ui", "pom.properties") =>
    MergeStrategy.singleOrError
  case x =>
    val oldStrategy = (assemblyMergeStrategy in assembly).value
    oldStrategy(x)
}