Generating OpenAPI documentation¶
To use, add the following dependencies:
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "0.19.0-M7"
"com.softwaremill.sttp.tapir" %% "tapir-openapi-circe-yaml" % "0.19.0-M7"
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 instance of OpenAPIDocsOptions
to the interpreter:
operationIdGenerator
: each endpoint corresponds to an operation in the OpenAPI format and should have a unique operation id. By default, thename
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.defaultDecodeFailureOutput
: if an endpoint does not define a Bad Request response inerrorOut
, tapir will try to guess if decoding of inputs may fail, and add a 400 response if necessary. You can override this option to customize the mapping of endpoint’s inputs to a default error response. If you’d like to disable this feature, just provide a function that always returnsNone
:OpenAPIDocsOptions.default.copy(defaultDecodeFailureOutput = _ => None)
Inlined and referenced schemas¶
All named schemas (that is, schemas which have the Schema.name
property defined) will be referenced at point of
use, and their definitions will be part of the components
section. If you’d like a schema to be inlined, instead
of referenced, modify the schema removing the name.
OpenAPI Specification Extensions¶
It’s possible to extend specification with extensions.
There are .docsExtension
methods available on Input/Output parameters and on endpoint
:
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._
import sttp.tapir.openapi._
import sttp.tapir.openapi.circe._
import sttp.tapir.openapi.circe.yaml._
import io.circe.generic.auto._
case class FruitAmount(fruit: String, amount: Int)
case class MyExtension(string: String, int: Int)
val sampleEndpoint =
endpoint.post
.in("path-hello" / path[String]("world").docsExtension("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 can be done using Swagger UI or
Redoc. The modules tapir-swagger-ui
and tapir-redoc
contain server endpoint
definitions, which given the documentation in yaml format, will expose it using the given context path. To use, add
as a dependency either:
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "0.19.0-M7"
"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "0.19.0-M7"
Then, you’ll need to pass the server endpoints to your server interpreter. For example, using akka-http:
import sttp.tapir._
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.tapir.openapi.circe.yaml._
import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter
import sttp.tapir.swagger.SwaggerUI
import scala.concurrent.Future
val myEndpoints: Seq[Endpoint[_, _, _, _]] = ???
val docsAsYaml: String = OpenAPIDocsInterpreter().toOpenAPI(myEndpoints, "My App", "1.0").toYaml
// add to your akka routes
val swaggerUIRoute = AkkaHttpServerInterpreter().toRoute(SwaggerUI[Future](docsAsYaml))
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)
}