# tapir

Rapid development of self-documenting APIs

tapir
## Intro Tapir is a library to describe HTTP APIs, expose them as a server, consume as a client, and automatically document using open standards. Tapir is fast and developer-friendly. The endpoint definition APIs are crafted with readability and discoverability in mind. Our Netty-based server is one of the best-performing Scala HTTP servers available. ```scala endpoint .get.in("hello").in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") ``` Tapir integrates with all major Scala stacks, so you can use your favorite approach to Functional Programming, while leveraging all the benefits that Tapir brings! Seamless integration with the Scala and HTTP ecosystems is one of Tapir's major strengths: * all popular Scala HTTP server implementations are supported. You can define your entire API using Tapir, or expose Tapir-managed routes alongside "native" ones. This is especially useful when gradually adopting Tapir, or using it for selected use-cases. * the Scala ecosystem is rich with libraries leveraging its type-safety and enhancing the developer's toolbox, that's why Tapir provides integrations with many of such custom type, JSON and observability libraries * documentation can be generated in the [OpenAPI](docs/openapi.md), [AsyncAPI](docs/asyncapi.md) and [JSON Schema](docs/json-schema.md) formats Depending on how you'd prefer to explore Tapir, this documentation has three main sections: 1. There's a number of [tutorials](tutorials/01_hello_world.md), which provide a gentle introduction to the library 2. Nothing compares to tinkering with working code, that's why we've prepared [runnable examples](examples.md), covering solutions to many "everyday" problems 3. Finally, the reference documentation describes all of Tapir's aspects in depth - take a look at the menu on the left, starting with the "Endpoints" section ScalaDocs are available at [javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.tapir). Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). ## Why tapir? * **type-safety**: compile-time guarantees, develop-time completions, read-time information * **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") * **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions * **observability**: leverage the metadata to report rich metrics and tracing information * **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs * **library, not a framework**: integrates with your stack ## Code teaser ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* type Limit = Int type AuthToken = String case class BooksQuery(genre: String, year: Int) case class Book(title: String) // Define an endpoint val booksListing: PublicEndpoint[(BooksQuery, Limit, AuthToken), String, List[Book], Any] = endpoint .get .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksQuery]) .in(query[Limit]("limit").description("Maximum number of books to retrieve")) .in(header[AuthToken]("X-Auth-Token")) .errorOut(stringBody) .out(jsonBody[List[Book]]) // Generate OpenAPI documentation import sttp.apispec.openapi.circe.yaml.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter val docs = OpenAPIDocsInterpreter().toOpenAPI(booksListing, "My Bookshop", "1.0") println(docs.toYaml) // Convert to pekko-http Route import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import org.apache.pekko.http.scaladsl.server.Route import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global def bookListingLogic(bfy: BooksQuery, limit: Limit, at: AuthToken): Future[Either[String, List[Book]]] = Future.successful(Right(List(Book("The Sorrows of Young Werther")))) val booksListingRoute: Route = PekkoHttpServerInterpreter() .toRoute(booksListing.serverLogic((bookListingLogic _).tupled)) // Convert to sttp Request import sttp.tapir.client.sttp4.SttpClientInterpreter import sttp.client4.* val booksListingRequest: Request[DecodeResult[Either[String, List[Book]]]] = SttpClientInterpreter() .toRequest(booksListing, Some(uri"http://localhost:8080")) .apply((BooksQuery("SF", 2016), 20, "xyz-abc-123")) ``` ## Other sttp projects sttp is a family of Scala HTTP-related projects, and currently includes: * [sttp client](https://github.com/softwaremill/sttp): the Scala HTTP client you always wanted! * sttp tapir: this project * [sttp model](https://github.com/softwaremill/sttp-model): simple HTTP model classes (used by client & tapir) * [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code. * [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models. When using AI agents, the [Scala skills](https://github.com/VirtusLab/scala-skill) that we maintain might be useful. ## Table of contents ```{eval-rst} .. toctree:: :maxdepth: 2 :caption: Getting started quickstart generate adopters support scala_2_3_platforms .. toctree:: :maxdepth: 2 :caption: Tutorials tutorials/01_hello_world tutorials/02_openapi_docs tutorials/03_json tutorials/04_errors tutorials/05_multiple_inputs_outputs tutorials/06_error_variants tutorials/07_cats_effect .. toctree:: :maxdepth: 2 :caption: How-to's examples external how-tos/delimited-path-parameters .. toctree:: :maxdepth: 2 :caption: Endpoints endpoint/basics endpoint/ios endpoint/oneof endpoint/codecs endpoint/customtypes endpoint/schemas endpoint/enumerations endpoint/validation endpoint/contenttype endpoint/json endpoint/pickler endpoint/xml endpoint/forms endpoint/security endpoint/streaming endpoint/websockets endpoint/integrations endpoint/static .. toctree:: :maxdepth: 2 :caption: Server interpreters server/overview server/netty server/http4s server/vertx server/armeria server/pekkohttp server/akkahttp server/ziohttp server/zio-http4s server/nima server/play server/finatra server/jdkhttp server/aws server/options server/path server/interceptors server/logic server/observability server/errors server/debugging .. toctree:: :maxdepth: 2 :caption: Client interpreters client/sttp client/sttp4 client/play client/http4s .. toctree:: :maxdepth: 2 :caption: Documentation interpreters docs/openapi docs/asyncapi docs/json-schema .. toctree:: :maxdepth: 2 :caption: Testing testing .. toctree:: :maxdepth: 2 :caption: Generators generator/sbt-openapi-codegen .. toctree:: :maxdepth: 2 :caption: Other subjects other/stability other/other_interpreters other/mytapir other/grpc other/troubleshooting other/migrating other/adr other/goals other/contributing # Quickstart To use tapir, add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-core" % "1.13.31" ``` This will import only the core classes needed to create endpoint descriptions. To generate a server or a client, you will need to add further dependencies. Many of tapir functionalities come as builder methods in the main package, hence it's easiest to work with tapir if you import the main package entirely, i.e.: ```scala import sttp.tapir.* ``` Finally, type: ```scala endpoint. ``` and see where auto-complete gets you! # Generate a Tapir project Not sure how to start? We recommend the defaults below (Direct-style stack & Netty server); this requires Java 21+. Otherwise, you might also try the Future stack + Netty, which works on Java 11+. If you'd like to include a JSON endpoint, using the [jsoniter](https://github.com/plokhotnyuk/jsoniter-scala) library might a good choice! ```{eval-rst} .. raw:: html ``` # Adopters Is your company already using tapir? We're continually expanding the "adopters" section in the documentation; the more the merrier! It would be great to feature your company's logo, but in order to do that, we'll need written permission to avoid any legal misunderstandings. Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. Thank you!
Adobe Swisscom Swissborg
Kaizo Process Street Tranzzo
Kelkoo group SoftwareMill Carvana
Moneyfarm Ocado Wegtam
Broad Kensu Colisweb
iceo dpg hunters
moia pits hootsuite
ematiq fugo budgetbakers
flo   xing
# Support & sponsorship ## Sponsors Development and maintenance of Tapir is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. We offer services around migrating and maintaining Java and Scala projects (e.g. to Java 21, or across Scala versions), ML/AI discovery workshops, introducing developer platforms (based on Kubernetes and observability technologies), and others. Our areas of expertise include performant backends, distributed systems, machine learning and data analytics, with a focus on Java, Scala, Kafka, TypeScript and Rust. [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) ## Commercial Support We offer commercial support for sttp and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer! # Scala 2, Scala 3; JVM, JS & Native Tapir is available for Scala 3.3+, Scala 2.13 and Scala 2.12, on the JVM, JS and Native platforms. Note that not all modules are available for all combinations of the above. This specifically applies to Scala.JS and Scala Native, where support is limited. The JVM modules require Java 11+, with a couple of exceptions, which require Java 21+ - this is marked in the documentation. ## In the documentation & examples The documentation & examples are written & compiled using Scala 3. To compile example code with Scala 2, some adjustments might be necessary: * For wildcard imports, use `_` instead of `*`, e.g. instead of `import sttp.tapir.*`, use `import sttp.tapir._` * For the main method, instead of `@main`, use an `object MyApp extends App`, e.g.: ```scala // in Scala 3: @main def myExample(): Unit = /* body */ // in Scala 2: object MyExample extends App { /* body */ } ``` * Instead of `given` definitions, use `implicit val` or `implicit def` (for codecs, schemas etc.). E.g.: ```scala // in Scala 3: given Schema[MyType] = Schema.derived // in Scala 2: implicit val myTypeSchema: Schema[MyType] = Schema.derived ``` * Use curly braces around class & method definitions. E.g.: ```scala // in Scala 3: class MyClass: def myMethod(): Unit = val z = 2 z + 2 // in Scala 2: class MyClass { def myMethod(): Unit = { val z = 2 z + 2 } } ``` ## Scala 2.12 Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually provide type arguments in some cases). In sbt, this is: ```scala scalacOptions += "-Ypartial-unification" ``` # 1. Hello, world! To start our adventure with tapir, we'll define and expose a single endpoint using an HTTP server. ```{note} The tutorial is also available [as a video](https://www.youtube.com/watch?v=WV1bZaGrdQQ). ``` ## Prerequisites We'll use [scala-cli](https://scala-cli.virtuslab.org) to run the code, so you'll need to install it beforehand. You can use any text editor to write the code, but we recommend using either [Metals](https://scalameta.org/metals/) if you're familiar with VSCode, or [IntelliJ IDEA](https://www.jetbrains.com/idea/) with the Scala plugin. You'll also need Java 21 or higher, as we'll be using virtual threads, which allow us to use a synchronous programming model, without sacrificing performance. If you don't have Java 21 installed, we recommend using [sdkman](https://sdkman.io/) to manage multiple Java versions locally. Going forward, we'll edit a `hello.scala` file. Let's start by adding the tapir dependency. First, you'll need the `tapir-core` module to describe the endpoint. Secondly, you'll need an HTTP server implementation. Tapir integrates with multiple servers, but we'll choose the simplest (and also one of the fastest!), which is based on Netty, available through the `tapir-netty-server-sync` module: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 ``` ## Endpoint description Once we have that, we can start describing our endpoint. This is done by taking an empty endpoint, available through the `sttp.tapir.endpoint` value, and gradually adding more details, such as the method, path, and other input/output parameters. Endpoint inputs are the values that are mapped to HTTP requests; endpoint outputs are the values that are mapped to HTTP responses. Inputs can be added to an existing endpoint description using the `Endpoint.in(...)` method, given the input description as a parameter. An updated endpoint description is returned. Input/output descriptions are created by methods available in the `sttp.tapir` package, hence it's often easiest to import it entirely, using `import sttp.tapir.*`. Let's start by defining the method and path of our endpoint: {emphasize-lines="4-11"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint .get .in("hello" / "world") println(helloWorldEndpoint.show) ``` You can now run the example from the command line. We are outputting a human-friendly description of the endpoint's structure, so you should see the following: ```bash % scala-cli hello.scala Compiling project (Scala 3.4.2, JVM (21)) Compiled project (Scala 3.4.2, JVM (21)) GET /hello /world -> -/- ``` So far, we've added three inputs to the endpoint: a constant method (`GET`), with `.get`; and two constant path inputs combined using `/`. Next, we'll add a query parameter input, but this time, it will extract the provided value instead of requiring it to be a fixed value (a constant): {emphasize-lines="10"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint .get .in("hello" / "world") .in(query[String]("name")) println(helloWorldEndpoint.show) ``` After running, the output should now be `GET /hello /world ?name -> -/-`. The `query[String]("name")` method creates a data structure describing a query parameter input. The description specifies that the value should be deserialized to a `String` - we'll learn how to deserialize to other data types in subsequent tutorials. Next, using `.in` we add this description to the data structure describing the endpoint as a whole. Finally, let's add an output to the endpoint. We'll return the response as a string body: {emphasize-lines="11"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint .get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) println(helloWorldEndpoint.show) ``` ## Server-side logic Let's add the logic to run once the endpoint is invoked. This can be done using the `.handleSuccess` method on the endpoint. We're using the "success" variant, since in this simple endpoint we don't differentiate between success and failure cases (200 and 4xx responses). The server logic needs to take the `String`, extracted from the query parameter, and return another `String`, which will be sent as a response: {emphasize-lines="12"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint .get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") println(helloWorldEndpoint.show) ``` Nothing changes in the output provided by `.show`, however the `helloWorldEndpoint` is now an instance of the `ServerEndpoint` class, which combines an endpoint description with a matching server logic. It's checked at compile-time that the shape of the server's logic function matches the types of inputs & outputs that we've defined in the endpoint! ## Exposing the server We can now expose the server to the outside world. First, we'll need to import the server implementation. Then, using the `NettySyncServer()` builder class, we can add endpoints, which the server should expose. In our example, we'll bind to `localhost` (which is the default), and to the port 8080: {emphasize-lines="5, 15-18"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint .get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") NettySyncServer() .port(8080) .addEndpoint(helloWorldEndpoint) .startAndWait() ``` The `startAndWait()` method blocks indefinitely. Once the above code compiles and runs successfully, we can test our endpoint: ```bash # first console % scala-cli hello.scala Compiling project (Scala 3.4.2, JVM (21)) Compiled project (Scala 3.4.2, JVM (21)) # another console % curl "http://localhost:8080/hello/world?name=Alice" Hello, Alice! ``` And that's it - our first tapir endpoint is exposed as an HTTP server! ## Recap In this tutorial, we learned the basic concepts needed to bootstrap a tapir-based application: * endpoints are defined as **values**, which describe the API. Such a description captures the **types** that are specified when creating the inputs/outputs. * before exposing an endpoint, **server logic** needs to be attached to the description. It's function that transforms the data extracted from the request, to data that will be used to create the response. It must match the types used for the inputs & outputs. * a **server** can be started by providing basic configuration and a list of server endpoints. # 2. Auto-generating OpenAPI docs ```{note} The tutorial is also available [as a video](https://www.youtube.com/watch?v=rfwEJvFZT28). ``` We already know how to expose an endpoint as an HTTP server. Let's now generate documentation for the API in the [OpenAPI](https://swagger.io/specification/) format, and expose it using the [Swagger UI](https://swagger.io). OpenAPI is a widely used format for describing HTTP APIs, which can be serialized to JSON or YAML. Such a description can be later used to generate client code in a given programming language, server stubs or programmer-friendly documentation. In our case, we'll use the last option, with the help of Swagger UI, which allows both browsing and invoking the endpoints. There are also alternative UIs, such as Redoc. Generating the OpenAPI specification and exposing the Swagger UI might be done separately. However, we'll use a bundle, which first interprets the provided tapir endpoints into OpenAPI and then returns another set of endpoints, which expose the UI together with the generated specification. We'll need to add a dependency: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 ``` We'll also define and expose two endpoints as an HTTP server, as described in the previous tutorial. Hence, our starting setup of `docs.scala` is as follows: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @main def tapirDocs(): Unit = val e1 = endpoint .get.in("hello" / "world").in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") val e2 = endpoint .post.in("double").in(stringBody) .errorOut(stringBody) .out(stringBody) .handle { s => s.toIntOption.fold(Left(s"$s is not a number"))(n => Right((n*2).toString)) } NettySyncServer().port(8080) .addEndpoints(List(e1, e2)) .startAndWait() ``` We've got two endpoints, one corresponding to `GET /hello/world`, the other a `POST /double`. We can test them from the command line as before: ```bash # first console % scala-cli docs.scala # another console % curl -XPOST "http://localhost:8080/double" -d "21" 42 % curl -XPOST "http://localhost:8080/double" -d "XYZ" XYZ is not a number ``` `NettySyncServer` is a server interpreter: it takes a list of endpoints (in our case, `List(e1, e2)`), and, basing on their description and the server logic, exposes them as an HTTP server. Similarly, a **documentation interpreter** takes a list of endpoints and, based on their description only (server logic is not needed here), generates the OpenAPI documentation. One possibility is to generate the YAML or JSON file, save it to disk, share it with other projects, etc. But in our case, as mentioned at the beginning, we'll use the rendered specification to expose a UI, allowing browsing our API. The UI itself needs to be exposed using HTTP. Hence, we'll need to generate tapir endpoints, which will serve the appropriate resources (such as the UI's `index.html`, the CSS files, and the generated OpenAPI specification). This amounts to invoking the `SwaggerInterpreter`: ```scala val swaggerEndpoints = SwaggerInterpreter() .fromEndpoints[Identity](List(e1, e2), "My App", "1.0") ``` By default, the generated endpoints will expose the UI using the `/docs` path, but this can be customized using the options. ```{note} You might wonder what is the purpose and meaning of the `Identity` type parameter, which is used when calling the `fromEndpoints` method. Tapir integrates with multiple Scala stacks, including Pekko, Akka, cats-effect, or ZIO. We'll learn how to use them in subsequent tutorials. They all use different types to represent effectful or asynchronous computations. So far, we've used direct-style, synchronous code, which doesn't use any "wrapper" types to represent computations. In some cases, tapir has dedicated APIs to work with direct-style, such as providing the server logic for an endpoint using `.handle` (when using the IO effect from cats-effect, server logic is provided using the `serverLogic[IO]` method). In other cases, there's a single set of APIs for direct and "wrapped" style - such as the API to generate the documentation & swagger endpoints above. The `Identity` type constructor is a simple type alias: `type Identity[X] = X`. It can be used whenever a type constructor parameter (typically called `F[_]`) is required, and when we're using direct-style, meaning that computations run synchronously in a blocking way. ``` And that's almost all the code changes that we need to introduce! We only need to additionally expose the `swaggerEndpoints` using our HTTP server: {emphasize-lines="3, 5, 8, 24-25, 29"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 import sttp.shared.Identity import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer import sttp.tapir.swagger.bundle.SwaggerInterpreter @main def tapirDocs(): Unit = val e1 = endpoint .get.in("hello" / "world").in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") val e2 = endpoint .post.in("double").in(stringBody) .out(stringBody) .errorOut(stringBody) .handle { s => s.toIntOption.fold(Left(s"$s is not a number"))(n => Right((n*2).toString)) } val swaggerEndpoints = SwaggerInterpreter() .fromServerEndpoints[Identity](List(e1, e2), "My App", "1.0") NettySyncServer().port(8080) .addEndpoints(List(e1, e2)) .addEndpoints(swaggerEndpoints) .startAndWait() ``` Try running the code, and opening [`http://localhost:8080/docs`](http://localhost:8080/docs) in your browser: ```bash % scala-cli docs.scala # Now open http://localhost:8080/docs in your browser ``` Browse the Swagger UI and invoke the endpoints. The generated OpenAPI specification should be available at [`http://localhost:8080/docs/docs.yaml`](http://localhost:8080/docs/docs.yaml): ```yaml openapi: 3.1.0 info: title: My App version: '1.0' paths: /hello/world: get: operationId: getHelloWorld parameters: - name: name in: query required: true schema: type: string responses: '200': description: '' content: text/plain: schema: type: string '400': description: 'Invalid value for: query parameter name' content: text/plain: schema: type: string /double: post: operationId: postDouble requestBody: content: text/plain: schema: type: string required: true responses: '200': description: '' content: text/plain: schema: type: string default: description: '' content: text/plain: schema: type: string ``` This wraps up the tutorial on generating and exposing OpenAPI documentation. If you'd like to customize some of the options, [tapir's OpenAPI reference documentation](../docs/openapi.md) should help. # 3. Using JSON bodies ```{note} The tutorial is also available [as a video](https://www.youtube.com/watch?v=NG8XWS7ijHU). ``` The endpoints we defined in the previous tutorials all used `String` bodies. Quite naturally, tapir supports much more than that - using appropriate **codecs**, it's possible to serialize and deserialize to arbitrary types. The most popular format on the web is JSON; hence, let's see how to expose a JSON-based endpoint using tapir. Tapir's support for JSON is twofold. First, we've got integrations with various JSON libraries, which provide the logic of converting between a `String` (that's read from the network) and a high-level type, such as a `case class`. Second, we've got the generation of **schemas**, which describe the high-level types. Schemas are used for documentation (so that our endpoints are described in OpenAPI accurately), and for validation of incoming requests. ## Deriving JSON codecs First, we need to pick a JSON library. There's a lot to choose from, but we'll go with [jsoniter](https://github.com/plokhotnyuk/jsoniter-scala), the fastest JSON library for Scala. We'll need to add a dependency, which will help us in defining the JSON codecs - we'll see how in a moment: ```scala //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 ``` Once we have that, let's define our data model, which we'll use for requests and responses. We'll define a single endpoint, transforming a `Meal` instance into a `Nutrition` one: {emphasize-lines="3-4"} ```scala //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 case class Meal(name: String, servings: Int, ingredients: List[String]) case class Nutrition(name: String, healthy: Boolean, calories: Int) ``` The first step is to define the functions that will enable the serialization and deserialization of these classes to JSON. This can be done by hand, but most of the time, we can rely on derivation: a compile-time process that generates the code needed to transform a `String` into a `Meal` (or an error) and to transform a `Nutrition` into a `String.` This is the task of our chosen JSON library. By adding a `... derives` clause, an instance of a `JsonValueCodec` will be generated at compile-time (with compile-time errors if the library can't figure out how to serialize/deserialize some component). We can also test the serialization of an example object. Let's put this in a `json.scala` file: ```scala //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.core.* // needed for `writeToString` import com.github.plokhotnyuk.jsoniter_scala.macros.* // needed for ... derives case class Meal(name: String, servings: Int, ingredients: List[String]) derives ConfiguredJsonValueCodec case class Nutrition(name: String, healthy: Boolean, calories: Int) derives ConfiguredJsonValueCodec @main def tapirJson(): Unit = println(writeToString(Meal("salad", 1, List("lettuce", "tomato", "cucumber")))) ``` ```{note} Even though we request derivation of a `ConfiguredJsonValueCodec`, we obtain a `JsonValueCodec` instance. This is due to the way jsoniter-scala works; the "configured" variant accepts an implicit `CodecMakerConfig`, which can be used to customize the (de)serialization process (`snake_case` vs `camelCase`, handling nulls, etc.). ``` This should output the following: ```bash % scala-cli json.scala {"name":"salad","servings":1,"ingredients":["lettuce","tomato","cucumber"]} ``` ## Deriving schema With the functions translating between JSON strings and our high-level types ready, we can take care of the second component: schemas. As mentioned in the beginning, schemas are needed to generate accurate OpenAPI documentation and validation. They can be defined by hand, but most of the time, you can use compile-time derivation - just as with JSON codecs. In our case, deriving the schemas will amount to adding a `... derives Schema` clause. Let's run a quick test: {emphasize-lines="1, 7, 10, 12, 16"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.core.* // needed for `writeToString` import com.github.plokhotnyuk.jsoniter_scala.macros.* // needed for ... derives import sttp.tapir.* // needed for `Schema` case class Meal(name: String, servings: Int, ingredients: List[String]) derives ConfiguredJsonValueCodec, Schema case class Nutrition(name: String, healthy: Boolean, calories: Int) derives ConfiguredJsonValueCodec, Schema @main def tapirJson(): Unit = println(writeToString(Meal("salad", 1, List("lettuce", "tomato", "cucumber")))) println(summon[Schema[Meal]]) ``` When run, we additionally get the schema: ```bash % scala-cli json.scala {"name":"salad","servings":1,"ingredients":["lettuce","tomato","cucumber"]} Schema(SProduct(List(SProductField(FieldName(name,name),Schema(SString(),None,false,None,None,None,None,false,false,All(List()),AttributeMap(Map()))), SProductField(FieldName(servings,servings),Schema(SInteger(),None,false,None,None,Some(int32),None,false,false,All(List()),AttributeMap(Map()))), SProductField(FieldName(ingredients,ingredients),Schema(SArray(Schema(SString(),None,false,None,None,None,None,false,false,All(List()),AttributeMap(Map()))),None,true,None,None,None,None,false,false,All(List()),AttributeMap(Map()))))),Some(SName(.Meal,List())),false,None,None,None,None,false,false,All(List()),AttributeMap(Map())) ``` As you can see, the string representation of the schema isn't the most beautiful, but its primary purpose is to be consumed by interpreters (e.g., the documentation interpreter), not by human beings. ## Exposing the endpoint We can now expose a JSON-based endpoint with both JSON codes and schemas in place. We'll try to read a `Meal` instance from the request and write a `Nutrition` instance as a response. In order to do this, we'll need to add a dependency which provides `tapir` <-> `jsoniter-scala` integration. The integration defines a `jsonBody` method that creates a description of a JSON body, which can be used both as an endpoint input and output. To create a `jsonBody[T]`, both a JSON codec and a `Schema[T]` must be in scope - and that's the case since these values are attached to the companion objects of `Meal` and `Nutrition`, thanks to the `... derives` mechanism. Notice how the `jsonBody[T]` method is used in the endpoint definition. We'll also expose Swagger UI documentation: {emphasize-lines="2-4, 10-15, 23-39"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.13.31 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* // needed for ... derives import sttp.tapir.* import sttp.tapir.json.jsoniter.* // needed for jsonBody[T] import sttp.tapir.server.netty.sync.NettySyncServer import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.shared.Identity import scala.util.Random case class Meal(name: String, servings: Int, ingredients: List[String]) derives ConfiguredJsonValueCodec, Schema case class Nutrition(name: String, healthy: Boolean, calories: Int) derives ConfiguredJsonValueCodec, Schema @main def tapirJson(): Unit = val random = new Random val mealEndpoint = endpoint.post .in(jsonBody[Meal]) .out(jsonBody[Nutrition]) // plugging in AI is left as an exercise to the reader .handleSuccess { meal => Nutrition(meal.name, random.nextBoolean(), random.nextInt(1000)) } val swaggerEndpoints = SwaggerInterpreter() .fromServerEndpoints[Identity](List(mealEndpoint), "My App", "1.0") NettySyncServer().port(8080) .addEndpoint(mealEndpoint) .addEndpoints(swaggerEndpoints) .startAndWait() ``` We can now test the endpoint both from the command line and via the browser: ```bash # first console % scala-cli json.scala # another console % curl -XPOST "http://localhost:8080" -d '{"name": "salad", "servings": 1, "ingredients": ["lettuce", "tomato", "cucumber"]}' {"name":"salad","healthy":true,"calories":42} # Now open http://localhost:8080/docs in your browser and browse the generated documentation! ``` Try to provide some invalid JSON values - you should see `400 Bad Request` responses. ## More on JSON To find out more about schema derivation and JSON support in tapir, the following reference documentation pages might be useful: * [](../endpoint/schemas.md) * [](../endpoint/json.md) # 4. Error handling ```{note} The tutorial is also available [as a video](https://www.youtube.com/watch?v=iXGJsk4_2Dg). ``` Many things can go wrong: that's why error handling is often the centerpiece of software libraries. We got a glimpse of one of Tapir's components when it comes to error handling when we discussed [adding OpenAPI documentation](02_openapi_docs.md). In this tutorial, we'll investigate Tapir's approach to error handling in more detail. Errors might be divided into "expected" errors: that is ones that we know how to handle, for which we have designed a specific response. These errors are most often caused by invalid input from the user (that is, invalid data that's part of the HTTP request). For such requests, we should return responses with error codes between 400 and 499, which are designated in the HTTP specification as "client errors". On the other hand, there are "unexpected errors", that we didn't foresee. When they occur, they signal some kind of problem with the server: a bug in the server's logic, hitting a limit of requests in progress, etc. When this happens, we should respond with a status code between 500 and 599, and log the error for the developer to inspect. These are "server errors". Which error codes exactly are returned, and what's the content of the response body that accompanies them is part of each endpoint's description and Tapir's configuration. ## Expected errors As we saw in previous tutorials, the description of an endpoint is a data structure, which contains the inputs (mapped to HTTP requests) & outputs (mapped to HTTP responses). The outputs of an endpoint describe what should happen on the "happy path" - when the server logic succeeds. Separately, the endpoint description can contain **error outputs**, which describe the shape of the HTTP response, in case an "expected error" occurs. Unless specified otherwise as part of the endpoint's description, when the HTTP response is generated using the successful outputs, the 200 status code is used; in case of error outputs, the status code is 400. Let's define an endpoint, which returns the JSON representation of the `Result` data type in case of success, and the JSON corresponding to the `Error` data type in case of an error. We'll be editing a `errors.scala` file. As in the previous tutorial, we'll be using Jsoniter to handle serialisation to JSON. We'll also need to derive the schemas both for the `Result` and `Error` classes, to represent them properly in documentation. Let's start by describing the endpoint: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.13.31 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* import sttp.tapir.* import sttp.tapir.json.jsoniter.* case class Result(v: Int) derives ConfiguredJsonValueCodec, Schema case class Error(description: String) derives ConfiguredJsonValueCodec, Schema @main def tapirErrors(): Unit = val maybeErrorEndpoint = endpoint.get .in("test") .in(query[Int]("input")) .out(jsonBody[Result]) .errorOut(jsonBody[Error]) ``` Just as calling `.out` on an endpoint description returns an updated endpoint description, with that output added, calling `.errorOut` returns a copy of the endpoint description, with an error output added. Each invocation of `.in`, `.out` and `.errorOut` accumulates inputs/outputs/error outputs. We can now add the server logic to the endpoint, using the `.handle` method. The result of that logic has to indicate if the result is a success, or an error. That's why the method which we'll need to provide has to return a value of type `Either[Error, Result]`. By convention, the left-side of an `Either` represents failure, and right-side success; we follow that in Tapir. Because endpoints are fully typed, it's statically checked by the compiler that we provide a server logic with types matching the endpoint's description; in our case, a function of type `Int => Either[Error, Result]`. We'll also add code to expose the endpoint as a server, along with its OpenAPI documentation: {emphasize-lines="2-3, 11-13, 24-28, 30-36"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.13.31 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* import sttp.tapir.* import sttp.tapir.json.jsoniter.* import sttp.tapir.server.netty.sync.NettySyncServer import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.shared.Identity case class Result(v: Int) derives ConfiguredJsonValueCodec, Schema case class Error(description: String) derives ConfiguredJsonValueCodec, Schema @main def tapirErrors(): Unit = val maybeErrorEndpoint = endpoint.get .in("test") .in(query[Int]("input")) .out(jsonBody[Result]) .errorOut(jsonBody[Error]) .handle { input => if input % 2 == 0 then Right(Result(input/2)) else Left(Error("That's an odd number!")) } val swaggerEndpoints = SwaggerInterpreter() .fromServerEndpoints[Identity](List(maybeErrorEndpoint), "My App", "1.0") NettySyncServer().port(8080) .addEndpoint(maybeErrorEndpoint) .addEndpoints(swaggerEndpoints) .startAndWait() ``` Let's run a couple of tests, to verify that our app does what we wanted: ```bash % curl -v "http://localhost:8080/test?input=10" < HTTP/1.1 200 OK < server: tapir/1.10.9 < Content-Type: application/json < content-length: 7 < {"v":5} % curl -v "http://localhost:8080/test?input=11" < HTTP/1.1 400 Bad Request < server: tapir/1.10.9 < Content-Type: application/json < content-length: 39 < {"description":"That's an odd number!"} ``` Works as designed! We get different JSONs and different status codes, depending on the result of the server logic. Also, take a look at [the docs](http://localhost:8080/docs) - they include both response variants, with 200 and 400 status codes. ## Unexpected errors Every now and then an exception pops up which we forget to properly handle. In such cases, the HTTP server of course continues to operate, but returns a 500-family response to the client. That's what happens in Tapir as well. By default, each server contains an **exception interceptor** which returns a `500 Internal Server Error` response, and logs the exception. We'll extend our previous example by an occasional unhandled exception being throw from our server logic. Additionally, we'll add Logback as a dependency, so that we get proper logging as part of the server's output. When you run the following, you'll see a lot of `DEBUG`-level logs (which can be turned off using `logback.xml`), but more importantly, you'll also get `ERROR` logs when unhandled exceptions happen: {emphasize-lines="6, 26"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.13.31 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 //> using dep ch.qos.logback:logback-classic:1.5.6 import com.github.plokhotnyuk.jsoniter_scala.macros.* import sttp.tapir.* import sttp.tapir.json.jsoniter.* import sttp.tapir.server.netty.sync.NettySyncServer import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.shared.Identity case class Result(v: Int) derives ConfiguredJsonValueCodec, Schema case class Error(description: String) derives ConfiguredJsonValueCodec, Schema @main def tapirErrors(): Unit = val maybeErrorEndpoint = endpoint.get .in("test") .in(query[Int]("input")) .out(jsonBody[Result]) .errorOut(jsonBody[Error]) .handle { input => if input % 3 == 0 then throw new RuntimeException("Multiplies of 3 are unacceptable!") if input % 2 == 0 then Right(Result(input/2)) else Left(Error("That's an odd number!")) } val swaggerEndpoints = SwaggerInterpreter() .fromServerEndpoints[Identity](List(maybeErrorEndpoint), "My App", "1.0") NettySyncServer().port(8080) .addEndpoint(maybeErrorEndpoint) .addEndpoints(swaggerEndpoints) .startAndWait() ``` Trying to invoke the endpoint results in a 500 status code: ```bash % curl -v "http://localhost:8080/test?input=9" < HTTP/1.1 500 Internal Server Error < server: tapir/1.10.9 < Content-Type: text/plain; charset=UTF-8 < content-length: 21 < Internal server error ``` And in the logs, we get the full details on what went wrong: ``` 16:18:14.355 [virtual-41] ERROR sttp.tapir.server.netty.sync.NettySyncServerOptions$ -- Exception when handling request: GET /test?input=9, by: GET /test, took: 18ms java.lang.RuntimeException: Multiplies of 3 are unacceptable! at errors$package$.$anonfun$15(errors.scala:26) ``` ## Further reading There's still a lot to cover on error handling in Tapir, and we'll go into more detail on some of the options in subsequent tutorials. For the impatient, you might be interested in the following reference documentation sections: * [error handling in server interpreters](../server/errors.md) * [one-of outputs](../endpoint/oneof.md) * [inputs/outputs, section on status codes](../endpoint/ios.md) # 5. Multiple inputs & outputs ```{note} The tutorial is also available [as a video](https://www.youtube.com/watch?v=rJAo9yZfr9k). ``` In the tutorials so far we've seen how to use endpoints which have a single input and a single output, optionally with an additional single error output. However, most commonly you'll have multiple inputs and outputs. This can include multiple path, query parameters and headers, accompanied by a body as inputs, along with multiple output headers, accompanied by a status code and body output. That's why in this tutorial we'll examine how to describe endpoints with multiple inputs/outputs and map them to high-level types. ## Describing the endpoint Adding multiple inputs/outputs is simply a matter of calling `.in` or `.out` on an endpoint description multiple times. To demonstrate how this works, let's describe an `/operation/{opName}?value1=...&value2=...` endpoint, where `opName` can be either `add` or `sub`, and `value1` and `value2` should be numbers. The result should be returned in the body, but additionally the hash of the result should be included in the `X-Result-Hash` custom header. Below is the endpoint description; we'll be editing the `multiple.scala` file: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 import sttp.tapir.* @main def tapirMultiple(): Unit = val opEndpoint = endpoint.get .in("operation" / path[String]("opName")) .in(query[Int]("value1")) .in(query[Int]("value2")) .out(stringBody) .out(header[String]("X-Result-Hash")) .errorOut(stringBody) ``` In our endpoint, we have: * 5 inputs: 1 constant method input (`.get`), 1 constant path input (`"operation"`), 1 path-segment-capturing input (`opName`), 2 query parameter inputs * 2 outputs: a body and a header * 1 error output: a string body, which we'll use in case an invalid operation name is provided ## Server logic When we provide the server logic for our endpoint, the values of the inputs are extracted from the HTTP request, and values from the output are mapped to the HTTP response. When there are multiple inputs, their values are by default extracted as a tuple. Conversely, the server logic must return a tuple, if there are multiple outputs. Only values of inputs which are non-constant contribute to the input tuple. That is, in our case, a 3-tuple will be extracted from the HTTP request: `(String, Int, Int)`, corresponding to the path segment and query parameters. The constant method & path inputs are used when matching an endpoint with an incoming request, but do not contribute to the extracted values. Here's the code with the server logic provided, transforming a `(String, Int, Int)` tuple to a `(String, String)` tuple. The output tuple is then mapped to the response body & header: {emphasize-lines="5, 8-9, 18-29"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @main def tapirMultiple(): Unit = def hash(result: Int): (String, String) = (result.toString, scala.util.hashing.MurmurHash3.stringHash(result.toString).toString) val opEndpoint = endpoint.get .in("operation" / path[String]("opName")) .in(query[Int]("value1")) .in(query[Int]("value2")) .out(stringBody) .out(header[String]("X-Result-Hash")) .errorOut(stringBody) // (String, Int, Int) => Either[String, (String, String)] .handle { (op, v1, v2) => op match case "add" => Right(hash(v1 + v2)) case "sub" => Right(hash(v1 - v2)) case _ => Left("Unknown operation. Available operations: add, sub") } NettySyncServer() .port(8080) .addEndpoint(opEndpoint) .startAndWait() ``` The user's input might be incorrect - that is, specify an unsupported operation name - in that case we return an error. That's why we need to wrap the result of the server logic either in a `Left` or `Right`, to use the error or success output. ```{note} In subsequent tutorials, we'll see how to better handle input parameters such as `opName` using enumerations. ``` We can now run some tests: ```bash # first console % scala-cli multiple.scala # second console % curl -v "http://localhost:8080/operation/add?value1=10&value2=14" < X-Result-Hash: 1385572155 24 % curl -v "http://localhost:8080/operation/sub?value1=10&value2=8" < X-Result-Hash: 382493853 2 ``` ## Mapping to case classes We could stop there, but the server logic's signature `(String, Int, Int) => Either[String, (String, String)]` isn't the most readable or developer-friendly. It would be much better (and less error-prone!) to use some custom data types, and avoid using raw `String`s and `Int`s everywhere. Let's define some case classes, which we'll use to capture the inputs and outputs, and give the parameters meaningful names: ```scala case class Input(opName: String, value1: Int, value2: Int) case class Output(result: String, hash: String) ``` Our goal now is to change the endpoint's description so that the server logic has the shape `Input => Either[String, Output]`. This can be done by mapping the inputs and outputs, that are defined on an endpoint, to our high-level types. For inputs, we can use the `.mapIn` method. We need to provide two-way conversions, between `(String, Int, Int)` and `Input`. The tuple => `Input` conversion is used for incoming requests: first the values are extracted, and then the mapping function is applied, yielding an `Input` instance, which is provided to the server logic. However, you also need to provide the `Input` => tuple conversion, in case the endpoint is interpreted as a client. We haven't covered this yet in the tutorials, but in the client-interpreter case such conversion is needed, when a request is sent. The mapping functions are simple, but quite boring to write: {emphasize-lines="8, 17-18, 23-27"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @main def tapirMultiple(): Unit = case class Input(opName: String, value1: Int, value2: Int) def hash(result: Int): (String, String) = (result.toString, scala.util.hashing.MurmurHash3.stringHash(result.toString).toString) val opEndpoint = endpoint.get .in("operation" / path[String]("opName")) .in(query[Int]("value1")) .in(query[Int]("value2")) .mapIn((opName, value1, value2) => Input(opName, value1, value2))(input => (input.opName, input.value1, input.value2)) .out(stringBody) .out(header[String]("X-Result-Hash")) .errorOut(stringBody) // Input => Either[String, (String, String)] .handle { input => input.opName match case "add" => Right(hash(input.value1 + input.value2)) case "sub" => Right(hash(input.value1 - input.value2)) case _ => Left("Unknown operation. Available operations: add, sub") } NettySyncServer() .port(8080) .addEndpoint(opEndpoint) .startAndWait() ``` The `.mapIn` method covers all inputs defined so far, hence we're calling it only after all inputs are defined. If we add more inputs later, the server logic will once again be parametrised by a tuple consisting of `Input` and the new inputs. ## Better mapping to case classes There is a better way of mapping multiple inputs and outputs to cases classes - Tapir can generate the "boring" mapping code for you. This can be done using `.mapInTo[]` and `.mapOutTo[]`. Both of these are macros, which take the target type as a parameter. The mapping code is generated at compile-time, verifying also at compile-time that the types of the inputs/outputs and the types specified as the case class parameters line up. Here's the modified code using `.mapInTo`, which additionally maps outputs to the `Output` class: {emphasize-lines="9, 11-13, 19, 22"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @main def tapirMultiple(): Unit = case class Input(opName: String, value1: Int, value2: Int) case class Output(result: String, hash: String) def hash(result: Int): Output = Output(result.toString, scala.util.hashing.MurmurHash3.stringHash(result.toString).toString) val opEndpoint = endpoint.get .in("operation" / path[String]("opName")) .in(query[Int]("value1")) .in(query[Int]("value2")) .mapInTo[Input] .out(stringBody) .out(header[String]("X-Result-Hash")) .mapOutTo[Output] .errorOut(stringBody) // Input => Either[String, Output] .handle { input => input.opName match case "add" => Right(hash(input.value1 + input.value2)) case "sub" => Right(hash(input.value1 - input.value2)) case _ => Left("Unknown operation. Available operations: add, sub") } NettySyncServer() .port(8080) .addEndpoint(opEndpoint) .startAndWait() ``` Now our server logic has the shape `Input => Either[String, Output]`. Much better! ## Further reading There's much more when it comes to supporting custom types in Tapir - a crucial feature for a type-safety-oriented library. We've seen how to use custom types using `jsonBody`, and now when combining multiple inputs and outputs. We'll see other ways in which custom types are supported in subsequent tutorials. As always, for the impatient, here's a couple of reference documentation links: * [](../endpoint/customtypes.md) * [](../endpoint/integrations.md) # 6. Error variants ```{note} The tutorial is also available [as a video](https://www.youtube.com/watch?v=w2ZL3WvhBZ8). ``` Quite often, there's more than one thing that might go wrong. On the other hand, success can also have many facets. In the previous tutorials we've seen that Tapir includes built-in support for differentiating between successful and error outputs. That's because in most cases the response that is returned in case of an error is totally different from a response returned in case of success. Hence, Tapir has built-in, top-level response variants: either error, or success. It's also possible to introduce more response variants, on lower levels, which further differentiate error and success scenarios. ## `oneOf` outputs Such differentiation of both success and error output can be achieved using `oneOf` output descriptions. As the name suggests, such outputs describe responses, which can take the shape of one of the given variants. Each variant is a description of an output, such as the ones that we've seen so far. We've also seen that each output describes a mapping between a high-level Scala type and the HTTP response. The same is true for `oneOf` outputs. Because `oneOf` has variants, we need a high-level type which also has variants. Each variant of the Scala type will correspond to one output variant. ```{note} For error and successful outputs we also have variants in the high-level type, `Either[E, O]`. There are two variants: `Left` and `Right`, corresponding to error and success outputs. ``` To represent various output variants on the Scala-value level, we'll typically use an `enum`. Each enum has a number of variants: exactly what we need. Mind that using an enum is not required when using `oneOf` outputs, just convenient. ## High-level response representation Let's start coding! We'll try to describe an endpoint, which fetches the avatar of the user. Here's a list of things that might go wrong: * unauthorized, in case the avatar of the requested user is not public * not found, in case there's no user with the provided id * other, in case the server logic would like to respond with a generic error And there's also a "list of things that might go right", meaning success variants: * found, with an array of bytes, containing the avatar * redirect, with an address where the avatar is located We'll represent both of these as an enum. We'll be editing the `variants.scala` file: ```scala enum AvatarError: case Unauthorized case NotFound case Other(msg: String) enum AvatarSuccess: case Found(bytes: Array[Byte]) case Redirect(location: String) ``` ## An output for a single variant Now that we have the high-level model in place, let's describe an output for a single variant; `AvatarSuccess.Redirect` is the most complicated one (we won't be using `oneOf` just yet!). In case of a redirect, we want the response to contain: * the `307 Temporary Redirect` status code * the `Location` header, with a value pointing to the avatar's location Endpoint outputs are described as instances of the `EndpointOutput` type. We've already seen output descriptions in previous tutorials; `stringBody` is an `EndpointOutput[String]`, and `jsonBody[Nutrition]` is an `EndpointOutput[Nutrition]`. Similarly, here, our goal is to obtain a value of type `EndpointOutput[AvatarSuccess.Redirect]`, which will be mapped to the status code & header described above. For the status code, we can use the constant status code output: `statusCode(StatusCode.TemporaryRedirect)`. It takes a `StatusCode` instance from the `sttp.model` package, and has the type `EndpointOutput[Unit]`. The `Unit` means that it doesn't map any high-level values to the response: it's a constant, and it always describes the same 307 status code. For the header, we have the `header[String](HeaderNames.Location)` output. Just as with query and path parameters that we've seen before, the `String` type parameter specifies that we'd like to serialize the header from a string. We can't request serializing `AvatarSuccess.Redirect` instances, as Tapir knows nothing about that type. Hence, here we'll have an `EndpointOutput[String]`: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* enum AvatarSuccess: case Found(bytes: Array[Byte]) case Redirect(location: String) val o1: EndpointOutput[Unit] = statusCode(StatusCode.TemporaryRedirect) val o2: EndpointOutput[String] = header[String](HeaderNames.Location) ``` We can combine these outputs into a composite output using the `EndpointOutput.and` method. This is similar to adding multiple outputs to an endpoint description using multiple `Endpoint.out` invocations. In fact, `Endpoint.out` internally using `EndpointOutput.and` to combine the endpoints defined so far. The type of the composite output corresponds to the values, that are mapped to the response. As `o1` doesn't map any values (it's a constant), the composite output will also have the type `EndpintOutput[String]`. Finally, we can map this output to the `AvatarSuccess.Redirect` type using `.mapTo`, which we've learned about last time: {emphasize-lines="12-13"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* enum AvatarSuccess: case Found(bytes: Array[Byte]) case Redirect(location: String) val o1: EndpointOutput[Unit] = statusCode(StatusCode.TemporaryRedirect) val o2: EndpointOutput[String] = header[String](HeaderNames.Location) val o3: EndpointOutput[String] = o1.and(o2) val o3mapped: EndpointOutput[AvatarSuccess.Redirect] = o3.mapTo[AvatarSuccess.Redirect] ``` ## Picking the right variant We're almost ready to define the `oneOf` output with variants. Each variant consists of two parts: the output, and a function determining (at run-time) if the variant should be used for a given high-level type. That is, when server logic returns an instance of the high-level type, we need to determine, which variant should be used to map it to the HTTP response. The default way to create variants is using the `oneOfVariant(EndpointOutput[T])` method. It creates a description of a variant, which will match all instances of the `T` type. This check is done by inspecting the run-time class of the `T` instance. This often works, but not always, as full type information is not always available at run-time, e.g. if `T` is a generic type. If that's the case, you'll get a compile-time error. However, for `AvatarSuccess`, this default way of creating variants works just fine, as we are dealing with enum cases, each of which translates to a separate class. Our one-of successful output takes the following form: {emphasize-lines="13-16"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* enum AvatarSuccess: case Found(bytes: Array[Byte]) case Redirect(location: String) val o1: EndpointOutput[Unit] = statusCode(StatusCode.TemporaryRedirect) val o2: EndpointOutput[String] = header[String](HeaderNames.Location) val successOutput: EndpointOutput[AvatarSuccess] = oneOf( oneOfVariant(o1.and(o2).mapTo[AvatarSuccess.Redirect]), oneOfVariant(byteArrayBody.mapTo[AvatarSuccess.Found]) ) ``` The `oneOf` output can be typed using the common parent of both variants, which is `AvatarSuccess`. The server logic will then have to return an instance of `AvatarSuccess`, in case of successful completion. ```{warning} Unfortunately, Tapir is not able to verify at compile-time that the variants are exhaustive, that is that every variant of the high-level type has a corresponding output-variant. ``` ## Dealing with singleton enum cases The output for `AvatarError` can be created similarly, with one caveat. It has two no-parameter cases (`Unauthorized` and `NotFound`), which are not translated into separate classes by the compiler. Hence, the run-time checks done by `oneOfVariant` would fail, or more precisely, any no-parameter case would be determined to match the first no-parameter-case output variant, yielding incorrect responses. To fix this, we can use the `oneOfVariantSingletonMatcher` method. It takes a unit-typed output, along with an exact value, to which the high-level output must be equal, for the variant to be chosen: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* enum AvatarError: case Unauthorized case NotFound case Other(msg: String) val errorOutput: EndpointOutput[AvatarError] = oneOf( oneOfVariantSingletonMatcher(statusCode(StatusCode.Unauthorized))(AvatarError.Unauthorized), oneOfVariantSingletonMatcher(statusCode(StatusCode.NotFound))(AvatarError.NotFound), oneOfVariant(stringBody.mapTo[AvatarError.Other]) ) ``` ## Describing the entire endpoint Equipped with `oneOf` outputs, we can now fully describe and test our endpoint: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.shared.Identity enum AvatarError: case Unauthorized case NotFound case Other(msg: String) enum AvatarSuccess: case Found(bytes: Array[Byte]) case Redirect(location: String) val o1: EndpointOutput[Unit] = statusCode(StatusCode.TemporaryRedirect) val o2: EndpointOutput[String] = header[String](HeaderNames.Location) val successOutput: EndpointOutput[AvatarSuccess] = oneOf( oneOfVariant(o1.and(o2).mapTo[AvatarSuccess.Redirect]), oneOfVariant(byteArrayBody.mapTo[AvatarSuccess.Found]) ) val errorOutput: EndpointOutput[AvatarError] = oneOf( oneOfVariantSingletonMatcher(statusCode(StatusCode.Unauthorized))(AvatarError.Unauthorized), oneOfVariantSingletonMatcher(statusCode(StatusCode.NotFound))(AvatarError.NotFound), oneOfVariant(stringBody.mapTo[AvatarError.Other]) ) @main def tapirErrorVariants(): Unit = val avatarEndpoint = endpoint.get .in("user" / "avatar") .in(query[Int]("id")) .out(successOutput) .errorOut(errorOutput) // Int => Either[AvatarError, AvatarSuccess] .handle { case 1 => Right(AvatarSuccess.Found(":-)".getBytes)) case 2 => Right(AvatarSuccess.Redirect("https://example.org/me.jpg")) case 3 => Left(AvatarError.Unauthorized) case 4 => Left(AvatarError.Other("We don't like this user.")) case _ => Left(AvatarError.NotFound) } val swaggerEndpoints = SwaggerInterpreter().fromServerEndpoints[Identity]( List(avatarEndpoint), "My App", "1.0") NettySyncServer() .port(8080) .addEndpoint(avatarEndpoint) .addEndpoints(swaggerEndpoints) .startAndWait() ``` As you can see, the server logic needs to return either an `AvatarError`, or a `AvatarSuccess`. This corresponds to the outputs that we have defined. Let's run a couple of tests: ```bash # first console % scala-cli variants.scala # second console % curl -v "http://localhost:8080/user/avatar?id=2" < HTTP/1.1 307 Temporary Redirect < server: tapir/1.10.10 < Location: https://example.org/me.jpg % curl -v "http://localhost:8080/user/avatar?id=7" < HTTP/1.1 404 Not Found % curl -v "http://localhost:8080/user/avatar?id=3" < HTTP/1.1 401 Unauthorized ``` We're also generating documentation. If you take a look at the [`http://localhost:8080/docs`](http://localhost:8080/docs), you'll see that each status code is properly documented. ## Further reading There's more ways to define `oneOf` variants, these are described in more detail on the reference page: [](../endpoint/oneof.md). # 7. Integration with cats-effect & http4s ```{note} The tutorial is also available [as a video](https://www.youtube.com/watch?v=M6ZHXM8_kaU). ``` [cats-effect](https://github.com/typelevel/cats-effect) is one of the most popular functional effect systems for Scala (probably also one of the top ones when it comes to pure functional programming in general). So far we've used Tapir in combination with so-called "direct style", where the server logic is expressed using synchronous, blocking code. However, one of Tapir's main strengths is that it integrates with virtually every Scala stack out there. This includes, first and foremost, cats-effect. Let's see how we can combine the two libraries together. ## Describing an endpoint We'll assume that you are familiar with what's described in the previous tutorials, especially [](01_hello_world.md). The good news is that most of what's described there applies 1-1 to our scenario, when we want to use cats-effect. The process of describing endpoints is identical, regardless of what programming style, Scala stack of effect library you use. Hence, we'll start with the same basic endpoint description. We'll be editing the `cats-effect.scala` file: ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 import sttp.tapir.* @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint .get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) ``` ```{note} As a side note, while our previous examples required Java 21+, as they leveraged virtual threads under the hood, the cats-effect version will work with Java 11+. ``` ## Server-side logic The crucial difference when using Tapir+cats-effect, as compared to the "direct" version is in the way the server logic is provided. The server logic does, most probably, involve side effects. Typically, this might be querying the database, writing to Kafka, or invoking other endpoints (though in our example, here we'll constrain ourselves to good old `println`s). Hence, any operations that the server logic performs should be captured using the `IO` monad. That is, given an endpoint with inputs of type `I` and error/success outputs of type `E` and `O`, the shape of the server logic function should be `I => IO[Either[E, O]]`. In other words: given the input parameters `I`, extracted from the request, the server logic should return a description of a computation, yielding either error `E` or success `O` outputs, which will then be mapped to the HTTP response. To combine an endpoint description with the server logic, we need to use the `.serverLogic` method. While not always required, as the type parameter is usually inferred correctly, it's nevertheless beneficial to provide the effect type parameter explicitly, using `.serverLogic[IO]` in our case: {emphasize-lines="2, 4, 12-14"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-cats:1.13.31 import cats.effect.IO import sttp.tapir.* @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint.get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) .serverLogic[IO](name => IO .println(s"Saying hello to: $name") .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) ``` The server-side logic consists of printing a message to the server's logs (`IO.println(...)`), followed by returning a pure value - successful result. The `s"Hello, $name"` string that will be mapped to the response needs to be first wrapped with a `Right` (as we want to use the successful outputs), and then lifted to an `IO` computation description using `IO.pure`. That way, we obtain a value of type `IO[Either[Unit, String]]`, as required by the endpoint description. ## http4s integration So far we've described the shape of the endpoint, and coupled it with a function implementing the server logic, with a matching signature. What we still need to do is to expose the endpoint using a server. The server must be "cats-effect-aware" - that is, it must need to know how to deal with server-side logic, which is expressed in terms of `IO` computation descriptions. So far we've been using the `NettySyncServer`, however here it won't be useful. Attempting to use it with our endpoint description won't compile, as there would be a mismatch on the type used to represent effects (`Identity` vs `IO`). Instead, we need to use a different server. Tapir provides a couple of choices (which might be useful depending on what you're already using in your project), but the most popular option in case of cats-effect is the [http4s](https://http4s.org) server. That's what we're going to do as well: through a Tapir-http4s integration, called a server interpreter. We've already introduced interpreters in the tutorial [](02_openapi_docs.md). In this particular case, the http4s server interpreter will convert our endpoint description+server logic into a `HttpRoutes[IO]` type. This type is the representation of HTTP routes that is understandable by the http4s API, and which can be used to expose a server to the outside world. The conversion process is an almost-one-liner (if it wasn't for line length limit!): {emphasize-lines="2, 5, 7, 18-19"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.31 import cats.effect.IO import org.http4s.HttpRoutes import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter @main def helloWorldTapir(): Unit = val helloWorldEndpoint = endpoint.get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) .serverLogic[IO](name => IO .println(s"Saying hello to: $name") .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() .toRoutes(helloWorldEndpoint) ``` ## Exposing the server As a final step, we need to expose the routes to the outside world. If you've ever used http4s, the following is fairly standard code to start a server and handle requests until the application is interrupted or killed: {emphasize-lines="3, 5, 7, 8, 12, 24-30"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.31 //> using dep org.http4s::http4s-blaze-server:0.23.16 import cats.effect.{ExitCode, IO, IOApp} import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter object HelloWorldTapir extends IOApp: val helloWorldEndpoint = endpoint.get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) .serverLogic[IO](name => IO .println(s"Saying hello to: $name") .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() .toRoutes(helloWorldEndpoint) override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) .resource .use(_ => IO.never) .as(ExitCode.Success) ``` First of all, you might notice that instead of the `@main` method, we are extending the `IOApp` trait. This is needed, because not only our endpoint's server logic is expressed using `IO`, but the entire process of starting the server and handling requests is described as an `IO` computation. Hence, we need to start the application in an `IO`-aware way: the `IOApp` will handle evaluating the `IO` description and actually running the code. Secondly, with http4s we need to use a specific server implementation (http4s itself is only an API to define endpoints - kind of a middle-man between Tapir and low-level networking code). We can choose from `blaze` and `ember` servers, here we're using the `blaze` one, which is reflected in the additional dependency and the server configuration constructor: `BlazeServerBuilder`. Finally, we've got the `run` method implementation, which attaches our interpreted route to the root context `/` and exposes the server on `localhost:8080`. ```{note} Note that you could also attach other, non-Tapir-managed routes to the same http4s application. Tapir-interpreted `HttpRoutes[IO]` can co-exist with routes defined in any other way. ``` ## Adding documentation As a final touch, let's expose documentation using the Swagger UI, just as we did before using the Netty server. The base process is the same: we first need to call the `SwaggerInterpreter` providing the list of endpoints, for which documentation should be generated. However, this time we'll provide the `IO` type constructor as the type parameter. That way, the server logic implementing the behavior of the swagger endpoints (such as reading the .js/.css/.html resources) will be expressed in terms of `IO`, and we'll be able to convert them later to http4s routes. And that's the second step that we need to perform: {emphasize-lines="3, 7, 13, 27-32, 37"} ```scala //> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.13.31 //> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.13.31 //> using dep org.http4s::http4s-blaze-server:0.23.16 import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.all.* import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.swagger.bundle.SwaggerInterpreter object HelloWorldTapir extends IOApp: val helloWorldEndpoint = endpoint.get .in("hello" / "world") .in(query[String]("name")) .out(stringBody) .serverLogic[IO](name => IO .println(s"Saying hello to: $name") .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() .toRoutes(helloWorldEndpoint) val swaggerEndpoints = SwaggerInterpreter() .fromServerEndpoints[IO](List(helloWorldEndpoint), "My App", "1.0") val swaggerRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoints) val allRoutes: HttpRoutes[IO] = helloWorldRoutes <+> swaggerRoutes override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> allRoutes).orNotFound) .resource .useForever ``` Hence, we first generate endpoint descriptions, which correspond to exposing the Swagger UI (containing the generated OpenAPI yaml for our `/hello/world` endpoint), which use `IO` to express their server logic. Then, we interpret those endpoints as `HttpRoutes[IO]`, which we can expose using http4's blaze server. ## Other concepts covered so far We can use JSON integration, error outputs, status codes, and any other Tapir features in the same way as we did so far with the "synchronous" server! The endpoints are described in the same way, the only thing that changes is how the server logic is provided. ## Further reading * [Netty-cats interpreter](../server/netty.md) * [Armeria-cats interpreter](../server/armeria.md) * [Integration with cats datatypes](../endpoint/customtypes.md) # Examples by category The Tapir repository contains a number of how-to guides. If you're missing an example for your use-case, please let us know by [reporting an issue](https://github.com/softwaremill/tapir)! Each example is fully self-contained and can be run using [scala-cli](https://scala-cli.virtuslab.org) (you just need to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make experimenting with Tapir as frictionless as possible! Examples are tagged with the stack being used (Direct-style, cats-effect, ZIO, Future), server implementation, generated documentation format etc. ```{eval-rst} ## Hello, World! * [A demo of Tapir's capabilities](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/booksExample.scala) circe sttp4 Swagger UI Future Pekko HTTP * [A demo of Tapir's capabilities](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala) Pickler sttp4 Swagger UI Future Netty * [Exposing an endpoint using the Armeria server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala) Future Armeria * [Exposing an endpoint using the Netty server (Direct-style variant)](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala) Direct Netty * [Exposing an endpoint using the Netty server (Future variant)](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala) Future Netty * [Exposing an endpoint using the Netty server (cats-effect variant)](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala) cats-effect Netty * [Exposing an endpoint using the Pekko HTTP server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala) Future Pekko HTTP * [Exposing an endpoint using the ZIO HTTP server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala) ZIO ZIO JSON ZIO HTTP * [Exposing an endpoint using the ZIO HTTP server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala) Swagger UI ZIO circe ZIO HTTP * [Exposing an endpoint using the built-in JDK HTTP server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala) Direct JDK Http * [Exposing an endpoint using the http4s server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala) cats-effect http4s * [Exposing an endpoint using the http4s server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala) Swagger UI ZIO circe http4s * [Exposing an endpoint, defined with ZIO and depending on services in the environment, using the http4s server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala) Swagger UI ZIO circe http4s * [Extending a base endpoint (which has the security logic provided), with server logic](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala) ZIO http4s ## Client interpreter * [Interpreting an endpoint as an http4s client](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala) circe cats-effect ## Custom types * [A demo of Tapir's capabilities using semi-auto derivation](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/custom_types/booksExampleSemiauto.scala) circe sttp4 Swagger UI Future Pekko HTTP * [A query parameter which maps to a Scala 3 enum (enumeration)](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/custom_types/enumQueryParameter.scala) Direct Netty * [Handling comma-separated query parameters](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala) Swagger UI Direct Netty * [Mapping a sealed trait hierarchy to JSON using a discriminator](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala) circe Swagger UI Direct Netty * [Supporting custom types, when used in query or path parameters, as well as part of JSON bodies](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala) circe ## Error handling * [Customising errors that are reported on decode failures (e.g. invalid or missing query parameter)](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala) Future Pekko HTTP * [Default error handler returning errors as JSON](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/errors/errorAsJson.scala) Future circe Pekko HTTP * [Error and successful outputs](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala) Future circe Pekko HTTP * [Error reporting provided by Iron type refinements](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala) circe cats-effect Netty * [Extending a base secured endpoint with error variants, using union types](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala) circe cats-effect http4s * [Optional returned from the server logic, resulting in 404 if None](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/errors/optionalValueExample.scala) circe Future Pekko HTTP ## JSON * [Receive JSON, parse it in a streaming way, expose documentation](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/json/jsoniterStreamingNettySyncServer.scala) jsoniter Swagger UI Direct Netty * [Return a JSON body which optionally serializes as `null`](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/json/circeNullBody.scala) circe Direct Netty * [Return a JSON response with Circe and auto-dervied codecs](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/json/circeAutoDerivationNettySyncServer.scala) circe Direct Netty * [Return a JSON response with Jsoniter](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/json/jsoniterNettySyncServer.scala) jsoniter Direct Netty ## Logging * [Logging using a correlation id](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala) ZIO Netty ## Multipart * [Uploading a multipart form, with text and file parts](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala) Future Pekko HTTP ## Observability * [OpenTelemetry tracing interceptor](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryTracingExample.scala) Direct circe Netty * [Otel4s collecting metrics](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/Otel4sMetricsExample.scala) cats-effect circe Netty * [Otel4s collecting traces](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/Otel4sTracingExample.scala) cats-effect circe Netty * [Reporting DataDog metrics](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala) Future circe Netty * [Reporting OpenTelemetry metrics](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala) Future circe Netty * [Reporting Prometheus metrics](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala) ZIO ZIO HTTP * [Reporting Prometheus metrics](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala) Future circe Netty * [Tracing requests with ZIO OpenTelemetry (customised config)](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/observability/ZIOpenTelemetryExample.scala) ZIO ZIO HTTP ## OpenAPI documentation * [Adding OpenAPI documentation extensions](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala) circe * [Documenting multiple endpoints](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala) Swagger UI cats-effect circe http4s * [Documenting multiple endpoints](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala) Swagger UI Future circe Pekko HTTP * [Exposing documentation using ReDoc](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala) ReDoc ZIO circe ZIO HTTP * [Exposing documentation using ReDoc](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala) ReDoc cats-effect http4s * [Exposing documentation using Scalar](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/ScalarZioHttpServer.scala) Scalar ZIO circe ZIO HTTP * [Exposing documentation using Scalar](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/ScalarContextPathHttp4sServer.scala) Scalar cats-effect http4s * [Securing Swagger UI using OAuth 2](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala) Swagger UI Future Pekko HTTP ## Schemas * [Customising a derived schema, using annotations, and using implicits](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala) Swagger UI Future circe Netty ## Security * [CORS interceptor](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala) Future Pekko HTTP * [CORS interceptor](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorVertxServer.scala) Future Vert.x * [HTTP basic authentication](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala) Future Pekko HTTP * [Interceptor verifying externally added security credentials](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala) Future Netty * [Login using OAuth2 with Google, authorization code flow](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GoogleNettySyncServer.scala) Direct Netty * [Login using OAuth2, authorization code flow](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala) cats-effect circe http4s * [Securing endpoint with CSRF tokens example](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/csrfTokens.scala) Future Netty * [Separating security and server logic, with a reusable base endpoint](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala) Future Pekko HTTP * [Separating security and server logic, with a reusable base endpoint, accepting & refreshing credentials via cookies](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala) Future Pekko HTTP * [Separating security and server logic, with a reusable base endpoint, accepting & refreshing credentials via cookies](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala) ZIO ZIO HTTP ## Server Sent Events * [Describe and implement an endpoint which emits SSE](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/sse/sseNettySyncServer.scala) Direct Netty * [Respond with either SSE or JSON](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/sse/sseOrJsonNettySyncServer.scala) Direct Netty ## Static content * [Serving static files from a directory](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala) Direct Netty * [Serving static files from a directory, with range requests](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesPekkoServer.scala) Future Pekko HTTP * [Serving static files from resources](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromResourcesPekkoServer.scala) Future Pekko HTTP * [Serving static files secured with a bearer token](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentSecurePekkoServer.scala) Future Pekko HTTP ## Status code * [Responding with fixed or dynamic status code](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala) Direct Netty ## Streaming * [Proxy requests, handling bodies as fs2 streams](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala) cats-effect http4s * [Respond with an fs2 stream, or with an error, represented as a failed effect in the business logic](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala) cats-effect http4s * [Stream request and response bodies as Ox Flows](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/streamingNettySyncServer.scala) Direct Netty * [Stream response as a Pekko stream](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala) Future Pekko HTTP * [Stream response as a ZIO stream](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala) ZIO Netty * [Stream response as a ZIO stream](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala) ZIO ZIO HTTP * [Stream response as an fs2 stream](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala) cats-effect http4s * [Stream response as an fs2 stream](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala) cats-effect Netty ## Testing * [Test endpoints using the MockServer client](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala) circe * [Test endpoints using the TapirStubInterpreter](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/testing/CatsServerStubInterpreter.scala) cats-effect * [Test endpoints using the TapirStubInterpreter](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreter.scala) Future Pekko HTTP ## WebSocket * [A WebSocket chat across multiple clients connected to the same server](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketChatNettySyncServer.scala) Direct Netty * [Describe and implement a WebSocket endpoint](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala) Direct Netty * [Describe and implement a WebSocket endpoint](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala) Future Pekko HTTP * [Describe and implement a WebSocket endpoint](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala) AsyncAPI cats-effect circe http4s * [Describe and implement a WebSocket endpoint, accepting and returning JSON messages](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketZioHttpJsonServer.scala) ZIO ZIO JSON ZIO HTTP * [Describe and implement a WebSocket endpoint, being notified on the server-side that a client closed the socket, using a custom codec](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketZioHttpCustomCodecServer.scala) ZIO ZIO HTTP :parser: markdown ``` # Articles, videos, other examples ## Generate a tapir project You can generate a simple tapir-based project using chosen features, build tool and effect system using [adopt-tapir](https://adopt-tapir.softwaremill.com). Alternatively, you can generate a stub of a tapir-based application directly from the command line with `sbt new softwaremill/tapir.g8`. ## Third-party examples * http4s interpreter: [todo-backend](https://github.com/lolgab/snunit-tapir-example) * quickstart using http4s: [a gitter8 template](https://codeberg.org/wegtam/http4s-tapir.g8). A new project can be created using: `sbt new https://codeberg.org/wegtam/http4s-tapir.g8.git` * Scala Native application, [using Nginx Unit](https://github.com/lolgab/snunit-tapir-example) * Sharing tapir endpoints between server and client, [using ZIO](https://github.com/dallinhuff/zio-tpilot) ## Project templates * [Bootzooka: direct-style, Scala3](https://github.com/softwaremill/bootzooka) * [Scala+ZIO+Quill+tapir, "real-world" implementation](https://github.com/softwaremill/realworld-tapir-zio) ## Blogs, articles * [WebSocket chat using structured concurrency, Ox & Tapir](https://softwaremill.com/websocket-chat-using-structured-concurrency-ox-and-tapir) * [A tapir looms in the distance](https://softwaremill.com/a-tapir-looms-in-the-distance/) * [Migrating from Akka HTTP to tapir](https://softwaremill.com/migrating-from-akka-http-to-tapir/) * [Benchmarking Tapir: Part 1](https://softwaremill.com/benchmarking-tapir-part-1/) * [Benchmarking Tapir: Part 2](https://softwaremill.com/benchmarking-tapir-part-2/) * [Tapir 1.0 released](https://softwaremill.com/tapir-1-0-released/) * [Security improvements in tapir 0.19](https://softwaremill.com/security-improvements-in-tapir-0-19/) * [Tapir serverless: a proof of concept](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) * [Designing tapir's WebSockets support](https://blog.softwaremill.com/designing-tapirs-websockets-support-ff1573166368) * [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e) * [tAPIr's Endpoint meets ZIO's IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) * [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0) * [Functional pancakes](https://blog.softwaremill.com/functional-pancakes-cf70023f0dcb) ## Videos * [Tapir tutorials: playlist](https://www.youtube.com/watch?v=WV1bZaGrdQQ&list=PL8NC5lCgGs6MnRHBdBNNWwafaHaxZTf9m&index=1&t=0s) * [Functional websockets](https://www.youtube.com/watch?v=rR7CK5xDn40) * [ScalaLove 2020: Your HTTP endpoints are data, as well!](https://www.youtube.com/watch?v=yuQNgZgSFIc&t=944s) * [Scalar 2020: A Functional Scala Stack For 2020](https://www.youtube.com/watch?v=DGlkap5kzGU) * [ScalaWorld 2019: Designing Programmer-Friendly APIs](https://www.youtube.com/watch?v=I3loMuHnYqw) # Handling Delimited Path Parameters Tapir allows you to handle complex path parameters, such as lists of custom types separated by delimiters (e.g., commas). This can be achieved using `Codec.delimited`, which facilitates the serialization and deserialization of delimited lists within path segments. ## Use Case Suppose you want to define an endpoint that accepts a list of names as a comma-separated path parameter. Each name should adhere to a specific pattern (e.g., only uppercase letters). ## Implementation Steps: ### 1. Define the Custom Type and Validator Start by defining your custom type and the associated validator to enforce the desired pattern. ```scala import sttp.tapir._ import sttp.tapir.generic.auto._ import sttp.tapir.Codec import sttp.tapir.Validator import sttp.tapir.CodecFormat.TextPlain import sttp.tapir.model.Delimited case class Name(value: String) // Validator to ensure names consist of uppercase letters only val nameValidator: Validator[String] = Validator.pattern("^[A-Z]+$") ``` ### 2. Create Codecs for the Custom Type and Delimited List Utilize `Codec.parsedString` for individual `Name` instances and `Codec.delimited` for handling the list. ```scala // Codec for single Name given Codec[String, Name, TextPlain] = Codec.parsedString(Name.apply) .validate(nameValidator.contramap(_.value)) // Codec for a list of Names, delimited by commas given Codec[String, Delimited[",", Name], TextPlain] = Codec.delimited ``` ### 3. Define the Endpoint with Delimited Path Parameter Incorporate the delimited codec into your endpoint definition to handle the list of names in the path. ```scala import sttp.tapir._ import sttp.tapir.generic.auto._ import sttp.tapir.Codec import sttp.tapir.Validator import sttp.tapir.CodecFormat.TextPlain import sttp.tapir.model.Delimited case class Name(value: String) // Validator to ensure names consist of uppercase letters only val nameValidator: Validator[String] = Validator.pattern("^[A-Z]+$") // Codec for single Name given Codec[String, Name, TextPlain] = Codec.parsedString(Name.apply) .validate(nameValidator.contramap(_.value)) // Codec for a list of Names, delimited by commas given Codec[String, Delimited[",", Name], TextPlain] = Codec.delimited val getUserEndpoint = endpoint.get .in("user" / path[Delimited[",", Name]]("id")) .out(stringBody) ``` ### 4. Generated OpenAPI Schema When you generate the OpenAPI documentation for this endpoint, the schema for the `id` path parameter will correctly reflect it as an array with the specified pattern for each item. ```yaml paths: /user/{id}: get: operationId: getUserId parameters: - name: id in: path required: true schema: type: array items: type: string pattern: ^[A-Z]+$ ``` ## Explanation - `Codec.parsedString`: Transforms a `String` into a custom type (`Name`) and vice versa. It also applies validation to ensure each `Name` adheres to the specified pattern. - `Codec.delimited`: Handles the serialization and deserialization of a delimited list (e.g., comma-separated) of the custom type. By specifying `Delimited[",", Name]`, Tapir knows how to split and join the list based on the delimiter. - Endpoint Definition: The `path[List[Name]]("id")` indicates that the id path parameter should be treated as a list of `Name` objects, utilizing the previously defined codecs. ## Validation Validators play a crucial role in ensuring that each element within the delimited list meets the required criteria. In this example, `nameValidator` ensures that each `Name` consists solely of uppercase letters. Tapir applies this validation to each element in the list, providing robust input validation. # Basics An endpoint is represented as a value of type `Endpoint[A, I, E, O, R]`, where: * `A` is the type of security input parameters * `I` is the type of input parameters * `E` is the type of error-output parameters * `O` is the type of output parameters * `R` are the capabilities that are required by this endpoint's inputs/outputs, such as support for websockets or a particular non-blocking streaming implementation. `Any`, if there are no such requirements. Input/output parameters (`A`, `I`, `E` and `O`) can be: * of type `Unit`, when there's no input/output * a single type * a tuple of types Hence, an empty, initial endpoint, with no inputs and no outputs, from which all other endpoints are derived has the type: ```scala import sttp.tapir.* val endpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = ??? ``` For endpoints which have no security inputs, a type alias is provided which fixes `A` to `Unit`: ```scala import sttp.tapir.* type PublicEndpoint[I, E, O, -R] = Endpoint[Unit, I, E, O, R] ``` A public endpoint that has two inputs of types `UUID` and `Int`, upon error returns a `String`, and on normal completion returns a `User`, would have the type: ```scala import sttp.tapir.* val userEndpoint: PublicEndpoint[(UUID, Int), String, User, Any] = ??? ``` You can think of an endpoint as a function which takes input parameters of type `A` and `I` and returns a result of type `Either[E, O]`. ## Infallible endpoints Note that the empty `endpoint` description maps no values to either error and success outputs, however errors are still represented and allowed to occur. In case of the error output, the single member of the unit type, `(): Unit`, maps to an empty-body `400 Bad Request`. If you prefer to use an endpoint description where errors cannot happen use `infallibleEndpoint: PublicEndpoint[Unit, Nothing, Unit, Any]`. This might be useful when interpreting endpoints [as a client](../client/sttp.md). ## Defining an endpoint The description of an endpoint is an immutable case class, which includes a number of methods: * the `name`, `description`, etc. methods allow modifying the endpoint information, which will then be included in the endpoint documentation * the `get`, `post` etc. methods specify the HTTP method which the endpoint should support * the `securityIn`, `in`, `errorOut` and `out` methods allow adding a new input/output parameter * `mapIn`, `mapInTo`, ... methods allow mapping the current input/output parameters to another value or to a case class An important note on mapping: in tapir, all mappings are bi-directional. That's because each mapping can be used to generate a server or a client, as well as in many cases can be used both for input and for output. ## Next Read on about describing [endpoint inputs/outputs](ios.md). # Codecs A `Codec[L, H, CF]` is a bi-directional mapping between low-level values of type `L` and high-level values of type `H`. Low level values are formatted as `CF`. A codec also contains the schema of the high-level value, which is used for validation and documentation. For example, a `Codec[String, User, CodecFormat.Json]` contains: * a function to decode a `String` into `User`, which assumes that the string if formatted as JSON; this decoding might fail of course in case of malformed input * a function to encode a `User` into a JSON `String`; this encoding step cannot fail There are built-in implicit codecs for most common types such as `String`, `Int`, `Instant` etc., as well as some types representing header values. Take a look at the `Codec` companion object for a full list. The companion object also contains a number of helper methods to create custom codecs. ## Looking up codecs For most inputs/outputs, the appropriate codec is required as an implicit parameter. Hence codec instances are usually defined as implicit values and resolved implicitly when they are referenced. However, they can also be provided explicitly as needed. As an example, a `query[Int]("quantity")` specifies an input parameter which corresponds to the `quantity` query parameter and will be mapped as an `Int`. A query input requires a codec, where the low-level value is a `List[String]` (representing potentially 0, one, or multiple parameters with the given name in the URL). Hence, an implicit `Codec[List[String], Int, TextPlain]` value will be looked up when using the `query` method (which is defined in the `sttp.tapir` package). In this example, the codec will verify that there's a single query parameter with the given name, and parse it as an integer. If any of this fails, a decode failure will be reported. However, in some cases codecs aren't looked up as implicit values, instead being created from simpler components, which themselves are looked up as implicits. This is the case e.g. for json bodies specified using `jsonBody`. The rationale behind such a design is that this provides better error reporting, in case the implicit components used to create the codec are missing. Consult the signature of the specific input/output to learn what are its implicit requirements. ## Decode failures In a server setting, if the value cannot be parsed as an int, a decoding failure is reported, and the endpoint won't match the request, or a `400 Bad Request` response is returned (depending on configuration). Take a look at [server error handling](../server/errors.md) for more details. ## Optional and multiple parameters Some inputs/outputs allow optional, or multiple parameters: * path segments are always required * query and header values can be optional or multiple (repeated query parameters/headers) * bodies can be optional, but not multiple In general, optional parameters are represented as `Option` values, and multiple parameters as `List` values. For example, `header[Option[String]]("X-Auth-Token")` describes an optional header. An input described as `query[List[String]]("color")` allows multiple occurrences of the `color` query parameter, with all values gathered into a list. ## Schemas A codec contains a schema, which describes the high-level type. The schema is used when generating documentation and enforcing validation rules. Schema consists of: * the schema type, which is one of the values defined in `SchemaType`, such as `SString`, `SBinary`, `SArray` or `SProduct`/`SCoproduct` (for ADTs). This is the shape of the encoded value - as it is sent over the network * meta-data: value optionality, description, example, default value and low-level format name * validation rules For primitive types, the schema values are built-in, and defined in the `Schema` companion object. The schema is left unchanged when mapping a codec, or an input/output, as the underlying representation of the value doesn't change. However, schemas can be changed for individual inputs/outputs using the `.schema(Schema)` method. Schemas are typically referenced indirectly through codecs, and are specified when the codec is created. As part of deriving a codec, to support a custom or complex type (e.g. for json mapping), schemas can be looked up implicitly and derived as well. See [custom types](customtypes.md) for more details. ## Codec format Codecs contain an additional type parameter, which specifies the codec format. Each format corresponds to a media type, which describes the low-level format of the raw value (to which the codec encodes). Some built-in formats include `text/plain`, `application/json` and `multipart/form-data`. Custom formats can be added by creating an implementation of the `sttp.tapir.CodecFormat` trait. Thanks to codecs being parametrised by codec formats, it is possible to have a `Codec[String, MyCaseClass, TextPlain]` which specifies how to serialize a case class to plain text, and a different `Codec[String, MyCaseClass, Json]`, which specifies how to serialize a case class to json. Both can be implicitly available without implicit resolution conflicts. Different codec formats can be used in different contexts. When defining a path, query or header parameter, only a codec with the `TextPlain` media type can be used. However, for bodies, any media type is allowed. For example, the input/output described by `jsonBody[T]` requires a json codec. ## Next Read on about [custom types](customtypes.md). # Content type The endpoint's output content type is bound to the body outputs, that are specified for the endpoint (if any). The [codec](codecs.md) of a body output contains a `CodecFormat`, which in turns contains the `MediaType` instance. ## Codec formats and server interpreters Codec formats define the *default* media type, which will be set as the `Content-Type` header. However, any user-provided value will override this default: * dynamic content type, using `.out(header(HeaderNames.ContentType))` * fixed content type, using e.g. `out(header(Header.contentType(MediaType.ApplicationJson)))` ## Multiple content types Multiple, alternative content types can be specified using [`oneOf`](oneof.md). On the server side, the appropriate mapping will be chosen using content negotiation, via the `Accept` header, using the [configurable](../server/options.md) `ContentTypeInterceptor`. Note that both the base media type, and the charset for text types are taken into account. On the client side, the appropriate mapping will be chosen basing on the `Content-Type` header value. For example: ```scala import sttp.tapir.* import sttp.tapir.Codec.{JsonCodec, XmlCodec} import sttp.model.StatusCode case class Entity(name: String) given JsonCodec[Entity] = ??? given XmlCodec[Entity] = ??? endpoint.out( oneOf( oneOfVariant(customCodecJsonBody[Entity]), oneOfVariant(xmlBody[Entity]) ) ) ``` For details on how to create codes manually or derive them automatically, see [custom types](customtypes.md) and the subsequent section on json. ## Next Read on about [json support](json.md). # Adding support for custom types To support a custom type, you'll need to provide an implicit `Codec` for that type, or the components to create such a codec. Most commonly, you'll be defining a custom codec so that a custom type can be used in inputs/outputs such as query parameters, path segments or headers. [Json](json.md) and [forms](forms.md) bodies have dedicated support for creating codecs, see the appropriate sections. A custom codec can be created by writing one from scratch, mapping over an existing codec, or automatically deriving one. Which of these approaches can be taken, depends on the context in which the codec will be used. ## Automatically deriving codecs In some cases, codecs can be automatically derived: * for supported [json](json.md) libraries * for urlencoded and multipart [forms](forms.md) * for value classes (extending `AnyVal`) Automatic codec derivation usually requires other implicits, such as: * json encoders/decoders from the json library * codecs for individual form fields * schema of the custom type, through the `Schema[T]` implicits (see the [next section on schemas](schemas.md)) Note that derivation of e.g. circe json encoders/decoders and tapir schemas are separate processes, and must be configured separately. ## Creating an implicit codec by hand To create a custom codec, you can either directly implement the `Codec` trait, which requires to provide the following information: * `encode` and `rawDecode` methods * schema (for documentation and validation) * codec format (`text/plain`, `application/json` etc.) This might be quite a lot of work; that's why it's usually easier to map over an existing codec. To do that, you'll need to provide two mappings: * a `decode` method which decodes the lower-level type into the custom type, optionally reporting decode failures (the return type is a `DecodeResult`) * an `encode` method which encodes the custom type into the lower-level type For example, to support a custom id type: ```scala import scala.util.* class MyId private (id: String): override def toString(): String = id object MyId: def parse(id: String): Try[MyId] = Success(new MyId(id)) ``` ```scala import sttp.tapir.* import sttp.tapir.CodecFormat.TextPlain def decode(s: String): DecodeResult[MyId] = MyId.parse(s) match case Success(v) => DecodeResult.Value(v) case Failure(f) => DecodeResult.Error(s, f) def encode(id: MyId): String = id.toString given Codec[String, MyId, TextPlain] = Codec.string.mapDecode(decode)(encode) ``` Or, using the type alias for codecs in the `TextPlain` format and `String` as the raw value: ```scala import sttp.tapir.Codec.PlainCodec given PlainCodec[MyId] = Codec.string.mapDecode(decode)(encode) ``` ```{note} Note that inputs/outputs can also be mapped over. In some cases, it's enough to create an input/output corresponding to one of the existing types, and then map over them. However, if you have a type that's used multiple times, it's usually better to define a codec for that type. ``` Then, you can use the new codec; e.g. to obtain an id from a query parameter, or a path segment: ```scala endpoint.in(query[MyId]("myId")) // or endpoint.in(path[MyId]) ``` ## Next Read on about [deriving schemas](schemas.md). # Enumerations tapir supports both `scala.Enumeration`-based enumerations, as well as enumerations created as a `sealed` family of `object`s (in Scala 2) or Scala 3 `enum`s where all cases are parameterless. Other enumeration implementations are also supported by integrating with [third-party libraries](integrations.md). Depending on the context, in which an enumeration is used, you'll need to create either a [`Schema`](schemas.md), or a [`Codec`](codecs.md) (which includes a schema). ## Using enumerations as values of query parameters, headers, path components When using an enumeration in such a context, a `Codec` has to be defined for the enumeration. tapir needs to know how to decode a low-level value into the enumeration, and how to encode an enumeration value into the low-level representation. This is handled by the codec's `decode` and `encode` functions. Moreover, each codec is associated with a schema (which describes the low-level representation for documentation). A schema, in turn, can have associated validators. In case of enumerations, a `Validator.Enumeration` should be added. The validator contains a list of all possible values (which are used when generating the docs). The enumeration validator doesn't provide any important run-time behavior, as if a value can be represented as an enumeration in the first place (by the process of decoding), it is valid. However, the codec's `decode` should return a `DecodeResult.InvalidValue` with a reference to the validator, if validation fails. This way, the [server](../server/errors.md) can provide appropriate user-friendly messages. ### scala.Enumeration support A default codec for any subtype of `scala.Enumeration#Value` is provided as an implicit/given value. Such a codec assumes that the low-level representation of the enumeration is a string. Encoding is done using `.toString`, while decoding performs a case-insensitive search through the enumeration's values. For example: ```scala import sttp.tapir.* object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") query[Features.Feature]("feature") ``` This can be customised (e.g. if the encoding/decoding should behave differently, or if the low-level representation should be a number), by defining an implicit codec: ```scala import sttp.tapir.Codec.PlainCodec implicit val customFeatureCodec: PlainCodec[Features.Feature] = Codec.derivedEnumerationValueCustomise[Int, Features.Feature]( { case 0 => Some(Features.A) case 1 => Some(Features.B) case 2 => Some(Features.C) case _ => None }, { case Features.A => 0 case Features.B => 1 case Features.C => 2 case _ => -1 }, None ) ``` ### Sealed families / enum support When the enumeration is defined as a sealed family containing only objects, or a Scala 3 `enum` with all cases parameterless, a codec has to be provided as an implicit value by hand. There is no implicit/given codec provided by default, as there's no way to constrain the type for which such an implicit would be considered by the compiler. For example: ```scala import sttp.tapir.* import sttp.tapir.Codec.PlainCodec sealed trait Feature object Feature: case object A extends Feature case object B extends Feature case object C extends Feature given PlainCodec[Feature] = Codec.derivedEnumeration[String, Feature].defaultStringBased query[Feature]("feature") ``` The `.defaultStringBased` method creates a default codec with decoding and encoding rules as described for the default `Enumeration` codec (using `.toString`). Such a codec can be similarly customised, by providing the `encode` and `decode` functions as parameters to the value returned to `derivedEnumeration`: ```scala import sttp.tapir.* import sttp.tapir.Codec.PlainCodec sealed trait Color case object Blue extends Color case object Red extends Color given PlainCodec[Color] = Codec.derivedEnumeration[String, Color]( (_: String) match { case "red" => Some(Red) case "blue" => Some(Blue) case _ => None }, _.toString.toLowerCase ) ``` ### Creating an enum codec by hand Creating an enumeration [codec](codecs.md) by hand is exactly the same as for any other type. The only difference is that an enumeration [validator](validation.md) has to be added to the codec's schema. Note that when decoding a value fails, it's best to return a `DecodeResult.InvalidValue`, with a reference to the enumeration validator. ### Lists of enumeration values If an input/output contains multiple enumeration values, delimited e.g. using a comma, you can look up a codec for `CommaSeparated[T]` or `Delimited[DELIMITER, T]` (where `D` is a type literal). The `Delimited` type is a simple wrapper for a list of `T`-values. For example, if the query parameter is required: ```scala import sttp.tapir.* import sttp.tapir.model.CommaSeparated object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") query[CommaSeparated[Features.Feature]]("features") ``` Additionally, the schema for such an input/output will have the `explode` parameter set to `false`, so that it is properly represented in [OpenAPI](../docs/openapi.md) documentation. You can take a look at a runnable example [here](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples/custom_types). ```{warning} `Delimited` and `CommaSeparated` rely on literal types, which are only available in Scala 2.13+. If you're using an older version of Scala, a workaround is creating a comma-separated codec locally. ``` ## Using enumerations as part of bodies When an enumeration is used as part of a body, on the tapir side you'll have to provide a [schema](schemas.md) for that type, so that the documentation is properly generated. Note, however, that the enumeration will also need to be properly supported by whatever means that body is parsed. If we have an [JSON](json.md) body, parsed with circe, you'll also need to provide circe's `Encoder` and `Decoder` implicits for the enumerations type, for the parsing to work properly. ### scala.Enumeration support A default schema for any subtype of `scala.Enumeration#Value` is provided as an implicit/given value. Such a schema assumes that the low-level representation of the enumeration is a string. Encoding is done using `.toString` (to represent the enumeration's values in the documentation). For example, to use an enum as part of a `jsonBody`, using the circe library for JSON parsing/serialisation, and automatic schema derivation for case classes: ```scala import io.circe.* import io.circe.generic.auto.* import sttp.tapir.* import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") case class Body(someField: String, feature: Features.Feature) // these need to be provided so that circe knows how to encode/decode enumerations - will work only in Scala2! given Decoder[Features.Feature] = Decoder.decodeEnumeration(Features) given Encoder[Features.Feature] = Encoder.encodeEnumeration(Features) // the schema for the body is automatically-derived, using the default schema for // enumerations (Schema.derivedEnumerationValue) jsonBody[Body] ``` A custom schema can be created by providing an alternate schema type (e.g. if the low-level representation of the enumeration is an integer), using `Schema.derivedEnumerationValueCustomise.apply(...)`. In this case, you'll need to provide the schema an implicit/given value: ```scala import sttp.tapir.* object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") given Schema[Features.Feature] = Schema.derivedEnumerationValueCustomise[Features.Feature]( encode = Some { case Features.A => 0 case Features.B => 1 case Features.C => 2 case _ => -1 }, schemaType = SchemaType.SInteger() ) ``` ### Sealed families / Scala3 enum support When the enumeration is defined as a sealed family containing only objects, or a Scala 3 `enum` with all cases parameterless, a schema has to be provided as an implicit/given value. There is no implicit/given schema provided by default, as there's no way to constrain the type for which such an implicit would be considered by the compiler. Moreover, when automatic [schema](schemas.md) derivation is used, the current implementation has no possibility to create the list of possible enumeration values (which is needed to create the enumeration validator). This might be changed in the future, but currently schemas for enumerations need to be created using `.derivedEnumeration`, instead of the more general `.derived`. For example: ```scala import sttp.tapir.* sealed trait Feature object Feature: case object A extends Feature case object B extends Feature case object C extends Feature given Schema[Feature] = Schema.derivedEnumeration[Feature].defaultStringBased ``` Similarly, using Scala 3's enums: ```scala enum ColorEnum: case Green extends ColorEnum case Pink extends ColorEnum given Schema.derivedEnumeration.defaultStringBased ``` You might need additional json-library-specific configuration, so that the Tapir & JSON configurations match. For example, in case of `jsoniter-scala`: ```scala given JsonValueCodec[ColorEnum] = JsonCodecMaker.make( CodecMakerConfig.withDiscriminatorFieldName(None) ) ``` This ensures that enum fields are parsed as plain strings instead of objects with a "type" discriminator field, e.g. `{"type": "Green"}` ### Scala 3 string-based constant union types to enum If a union type is a string-based constant union type, it can be auto-derived as field type or manually derived by using the `Schema.derivedStringBasedUnionEnumeration[T]` method. Constant strings can be derived by using the `Schema.constStringToEnum[T]` method. Examples: ```scala val aOrB: Schema["a" | "b"] = Schema.derivedStringBasedUnionEnumeration ``` ```scala val a: Schema["a"] = Schema.constStringToEnum ``` ```scala case class Foo(aOrB: "a" | "b", optA: Option["a"]) derives Schema ``` ### Creating an enum schema by hand Creating an enumeration [schema](schemas.md) by hand is exactly the same as for any other type. The only difference is that an enumeration [validator](validation.md) has to be added to the schema. ## Next Read on about [validation](validation.md). # Forms ## URL-encoded forms An URL-encoded form input/output can be specified in two ways. First, it is possible to map all form fields as a `Seq[(String, String)]`, or `Map[String, String]` (which is more convenient if fields can't have multiple values): ```scala import sttp.tapir.* formBody[Seq[(String, String)]]: EndpointIO.Body[String, Seq[(String, String)]] formBody[Map[String, String]]: EndpointIO.Body[String, Map[String, String]] ``` Second, form data can be mapped to a case class. The codec for the case class is automatically derived using a macro at compile-time. The fields of the case class should have types, for which there is a plain text codec. For example: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* case class RegistrationForm(name: String, age: Int, news: Boolean, city: Option[String]) formBody[RegistrationForm]: EndpointIO.Body[String, RegistrationForm] ``` Each form-field is named the same as the case-class-field. The names can be transformed to snake or kebab case by providing an implicit `tapir.generic.Configuraton`, or customised using the `@encodedName` annotation. ## Multipart forms Similarly as above, multipart form input/outputs can be specified in two ways. To map to all parts of a multipart body, use: ```scala import sttp.tapir.* import sttp.model.Part multipartBody: EndpointIO.Body[Seq[RawPart], Seq[Part[Array[Byte]]]] ``` `Part` is a case class containing the `name` of the part, disposition parameters, headers, and the body. The bodies will be mapped as byte arrays (`Array[Byte]`). Custom multipart codecs can be defined with the `Codec.multipartCodec` method, and then used with `multipartBody[T]`. As with URL-encoded forms, multipart bodies can be mapped directly to case classes, however without the restriction on codecs for individual fields. Given a field of type `T`, first a plain text codec is looked up, and if one isn't found, any codec for any media type (e.g. JSON) is searched for. Each part is named the same as the case-class-field. The names can be transformed to snake or kebab case by providing an implicit `sttp.tapir.generic.Configuraton`, or customised using the `@encodedName` annotation. Additionally, the case class to which the multipart body is mapped can contain both normal fields, and fields of type `Part[T]`. This is useful, if part metadata (e.g. the filename) is relevant. For example: ```scala import sttp.tapir.* import sttp.model.Part import java.io.File import sttp.tapir.generic.auto.* case class RegistrationForm(userData: User, photo: Part[File], news: Boolean) case class User(email: String) multipartBody[RegistrationForm]: EndpointIO.Body[Seq[RawPart], RegistrationForm] ``` The fields can also be wrapped into `Option[T]` or `Option[Part[T]]` if the part is not required. If there can be none or multiple parts for the same name, the fields can be wrapped into `List[T]` or `List[Part[T]]` or any other container `C` for which exists a codec `List[T] => C[T]` ```scala import sttp.tapir.* import sttp.model.Part import java.io.File import sttp.tapir.generic.auto.* case class RegistrationForm(userData: Option[User], photos: List[Part[File]], news: Option[Part[Boolean]]) case class User(email: String) multipartBody[RegistrationForm]: EndpointIO.Body[Seq[RawPart], RegistrationForm] ``` ```{warning} When receiving a multipart request in a Tapir server, any files created to store multipart parts will be removed after the request processing completes (regardless of the outcome - HTTP success, HTTP failure or exception). ``` ## Next Read on about [security](security.md). # Third-party datatype libraries integrations ```{note} Note that the codecs defined by the tapir integrations are used only when the specific types (e.g. enumerations) are used at the top level. Any nested usages (e.g. as part of a json body), need to be separately configured to work with the used json library. ``` ## Cats datatypes integration The `tapir-cats` module contains additional instances for some [cats](https://typelevel.org/cats/) datatypes as well as additional syntax: ```scala "com.softwaremill.sttp.tapir" %% "tapir-cats" % "1.13.31" ``` - `import sttp.tapir.integ.cats.codec.*` - brings schema, validator and codec instances - `import sttp.tapir.integ.cats.syntax.*` - brings additional syntax for `tapir` types Additionally, the `tapir-cats-effect` module contains an implementation of the `CatsMonadError` class, providing a bridge between the sttp-internal `MonadError` and the cats-effect `Sync` typeclass: ```scala "com.softwaremill.sttp.tapir" %% "tapir-cats-effect" % "1.13.31" ``` ## Refined integration If you use [refined](https://github.com/fthomas/refined), the `tapir-refined` module will provide implicit codecs and validators for `T Refined P` as long as a codec for `T` already exists: ```scala "com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.13.31" ``` You'll need to extend the `sttp.tapir.codec.refined.TapirCodecRefined` trait or `import sttp.tapir.codec.refined.*` to bring the implicit values into scope. The refined codecs contain a validator which wrap/unwrap the value from/to its refined equivalent. Some predicates will bind correctly to the vanilla tapir Validator, while others will bind to a custom validator that might not be very clear when reading the generated documentation. Correctly bound predicates can be found in `integration/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala`. If you are not satisfied with the validator generated by `tapir-refined`, you can provide an implicit `ValidatorForPredicate[T, P]` in scope using `ValidatorForPredicate.fromPrimitiveValidator` to build it (do not hesitate to contribute your work!). ## Iron integration If you use [iron](https://github.com/Iltotore/iron), the `tapir-iron` module will provide implicit codecs and validators for `T :| P` as long as a codec for `T` already exists: ```scala "com.softwaremill.sttp.tapir" %% "tapir-iron" % "1.13.31" ``` The module is only available for Scala 3 since iron is not designed to work with Scala 2. You'll need to extend the `sttp.tapir.codec.refined.TapirCodecIron` trait or `import sttp.tapir.codec.iron.*` to bring the implicit values into scope. The iron codecs contain a validator which apply the constraint to validated value. Similarly to `tapir-refined`, you can find the predicate logic in `integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala` and provide your own given `ValidatorForPredicate[T, P]` in scope using `ValidatorForPredicate.fromPrimitiveValidator` ### Validation When using `iron` in the server e.g. in case classes that JSON request body is parsed to, some additional steps need to be taken to properly report `iron` validation errors. [Iron](https://github.com/Iltotore/iron) is operating on type level while regular tapir validation works on case classes created from parsed JSON. When `iron` types are used in a case class, and passed values are invalid for `iron` types, creation is impossible because `iron` does not allow creating guarded type instance. Because it is not possible to create case class for `ServerInterpreter` it looks like JSON parsing error not like validation error. In such case no error message is displayed to user. To properly report `iron` errors it is necessary to recognize them in failure intereptor. Custom JSON parsing is necessary anyway so custom exception can be thrown in case of `iron` refinement error and then matched in failure interceptor. Example for `circe`: ```scala case class IronException(error: String) extends Exception(error) inline given (using inline constraint: Constraint[Int, Positive]): Decoder[Age] = summon[Decoder[Int]].map(unrefinedValue => unrefinedValue.refineEither[Positive] match case Right(value) => value case Left(errorMessage) => throw IronException(s"Could not refine value $unrefinedValue: $errorMessage") ) ``` Then failure handler matching `IronException` is needed. Remember to create the interceptor: ```scala private def failureDetailMessage(failure: DecodeResult.Failure): Option[String] = failure match { case Error(_, JsonDecodeException(_, IronException(errorMessage))) => Some(errorMessage) case Error(_, IronException(errorMessage)) => Some(errorMessage) case other => FailureMessages.failureDetailMessage(other) } private def failureMessage(ctx: DecodeFailureContext): String = { val base = FailureMessages.failureSourceMessage(ctx.failingInput) val detail = failureDetailMessage(ctx.failure) FailureMessages.combineSourceAndDetail(base, detail) } def ironFailureHandler[T[_]] = new DefaultDecodeFailureHandler[T]( DefaultDecodeFailureHandler.respond, failureMessage, DefaultDecodeFailureHandler.failureResponse ) def ironDecodeFailureInterceptor[T[_]] = new DecodeFailureInterceptor[T](ironFailureHandler[T]) ``` ...and add it to server options: ```scala override def run = NettyCatsServer .io() .use { server => // Don't forget to add the interceptor to server options val optionsWithInterceptor = server.options.prependInterceptor(ironDecodeFailureInterceptor) for { binding <- server .port(port) .host(host) .options(optionsWithInterceptor) .addEndpoint(endpoint) .start() //... } } ``` ## Enumeratum integration The `tapir-enumeratum` module provides schemas, validators and codecs for [Enumeratum](https://github.com/lloydmeta/enumeratum) enumerations. To use, add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % "1.13.31" ``` Then, `import sttp.tapir.codec.enumeratum.*`, or extends the `sttp.tapir.codec.enumeratum.TapirCodecEnumeratum` trait. This will bring into scope implicit values for values extending `*EnumEntry`. ## NewType integration If you use [scala-newtype](https://github.com/estatico/scala-newtype), the `tapir-newtype` module will provide implicit codecs and schemas for types with a `@newtype` and `@newsubtype` annotations as long as a codec and schema for its underlying value already exists: ```scala "com.softwaremill.sttp.tapir" %% "tapir-newtype" % "1.13.31" ``` Then, `import sttp.tapir.codec.newtype.*`, or extend the `sttp.tapir.codec.newtype.TapirCodecNewType` trait to bring the implicit values into scope. ## Monix NewType integration If you use [monix newtypes](https://github.com/monix/newtypes), the `tapir-monix-newtype` module will provide implicit codecs and schemas for types which extend `NewtypeWrapped` and `NewsubtypeWrapped` annotations as long as a codec and schema for its underlying value already exists: ```scala "com.softwaremill.sttp.tapir" %% "tapir-monix-newtype" % "1.13.31" ``` Then, `import sttp.tapir.codec.monix.newtype.*`, or extend the `sttp.tapir.codec.monix.newtype.TapirCodecMonixNewType` trait to bring the implicit values into scope. ## ZIO Prelude Newtype integration If you use [ZIO Prelude Newtypes](https://zio.github.io/zio-prelude/docs/newtypes/), the `tapir-zio-prelude` module will provide implicit codecs and schemas for types defined using `Newtype` and `Subtype` as long as a codec and a schema for the underlying type already exists: ```scala "com.softwaremill.sttp.tapir" %% "tapir-zio-prelude" % "1.13.31" ``` Then, mix in `sttp.tapir.codec.zio.prelude.newtype.TapirNewtypeSupport` into your newtype to bring the implicit values into scope: ```scala import sttp.tapir.Codec.PlainCodec import sttp.tapir.Schema import sttp.tapir.codec.zio.prelude.newtype.TapirNewtypeSupport import zio.prelude.Newtype object Foo extends Newtype[String] with TapirNewtypeSupport[String] type Foo = Foo.Type implicitly[Schema[Foo]] implicitly[PlainCodec[Foo]] ``` Or use the `TapirNewtype` helper to derive a codec or a schema without modifying the newtype: ```scala import sttp.tapir.codec.zio.prelude.newtype.TapirNewtype object Bar extends Newtype[String] type Bar = Bar.Type // Explicitly provide the base type of your newtype when instantiating the helper, in this case, String. val BarSupport = TapirNewtype[String](Bar) import BarSupport.* implicitly[Schema[Bar]] implicitly[PlainCodec[Bar]] ``` ## Derevo integration The `tapir-derevo` module provides a way to derive schema for your type using `@derive` annotation. For details refer to [derevo documentation](https://github.com/tofu-tf/derevo#installation). To use, add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-derevo" % "1.13.31" ``` Then you can derive schema for your ADT along with other typeclasses besides ADT declaration itself: ```scala import derevo.derive import sttp.tapir.derevo.schema @derive(schema) case class Person(name: String, age: Int) //or with custom description @derive(schema("Type of currency in the country")) sealed trait Currency object Currency: case object CommunisticCurrency extends Currency case class USD(amount: Long) extends Currency ``` The annotation will simply generate a `Schema[T]` for your type `T` and put it into companion object. Generation rules are the same as in `Schema.derived[T]`. This will also work for newtypes — [estatico](https://github.com/estatico/scala-newtype) or [supertagged](https://github.com/rudogma/scala-supertagged): ```scala import derevo.derive import sttp.tapir.derevo.schema import io.estatico.newtype.macros.newtype object types: @derive(schema) @newtype case class Amount(i: Int) ``` Resulting schema will be equivalent to `implicitly[Schema[Int]].map(i => Some(types.Amount(i)))`. Note that due to limitations of the `derevo` library one can't provide custom description for generated schema. ## Next Read on about [serving static content](static.md). # Inputs/outputs An input is described by an instance of the `EndpointInput` trait, and an output by an instance of the `EndpointOutput` trait. Some inputs can be used both as inputs and outputs; then, they additionally implement the `EndpointIO` trait. Each input or output can yield/accept a value (but doesn't have to). For example, `query[Int]("age"): EndpointInput[Int]` describes an input, which is the `age` parameter from the URI's query, and which should be coded (using the string-to-integer [codec](codecs.md)) as an `Int`. The `tapir` package contains a number of convenience methods to define an input or an output for an endpoint. For inputs, these are: * `path[T]`, which captures a path segment as an input parameter of type `T` * any string, which will be implicitly converted to a fixed path segment. Constant path segments can be combined with the `/` method, and don't map to any values (they have type `EndpointInput[Unit]`, but they still modify the endpoint's behavior) * `paths`, which maps to the whole remaining path as a `List[String]` * `query[T](name)` captures a query parameter with the given name * `queryParams` captures all query parameters, represented as `QueryParams` * `cookie[T](name)` captures a cookie from the `Cookie` header with the given name * `extractFromRequest` extracts a value from the request. This input is only used by server interpreters, ignored by documentation interpreters. Client interpreters ignore the provided value. It can also be used to access the original request through the `underlying: Any` field. For both inputs/outputs: * `header[T](name)` captures a header with the given name * `header[T](name, value)` maps to a fixed header with the given name and value * `headers` captures all headers, represented as `List[Header]` * `cookies` captures cookies from the `Cookie` header and represents them as `List[Cookie]` * `setCookie(name)` captures the value & metadata of the a `Set-Cookie` header with a matching name * `setCookies` captures cookies from the `Set-Cookie` header and represents them as `List[SetCookie]` * `stringBody`, `plainBody[T]`, `jsonBody[T]`, `rawBinaryBody[R]`, `binaryBody[R, T]`, `formBody[T]`, `multipartBody[T]`, `fileBody` captures the body * `streamBody[S]` captures the body as a stream: only a client/server interpreter supporting streams of type `S` can be used with such an endpoint * `oneOfBody` captures multiple variants of bodies representing the same content, but using different content types For outputs: * `statusCode` maps to the status code of the response * `statusCode(code)` maps to a fixed status code of the response ## Combining inputs and outputs Endpoint inputs/outputs can be combined in two ways. However they are combined, the values they represent always accumulate into tuples of values. First, inputs/outputs can be combined using the `.and` method. Such a combination results in an input/output which maps to a tuple of the given types. This combination can be assigned to a value and re-used in multiple endpoints. As all other values in tapir, endpoint input/output descriptions are immutable. For example, an input specifying two query parameters, `start` (mandatory) and `limit` (optional) can be written down as: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* import java.util.UUID case class User(name: String) val paging: EndpointInput[(UUID, Option[Int])] = query[UUID]("start").and(query[Option[Int]]("limit")) // we can now use the value in multiple endpoints, e.g.: val listUsersEndpoint: PublicEndpoint[(UUID, Option[Int]), Unit, List[User], Any] = endpoint.in("user" / "list").in(paging).out(jsonBody[List[User]]) ``` Second, inputs can be combined by calling the `in`, `out` and `errorOut` methods on `Endpoint` multiple times. Each time such a method is invoked, it extends the list of inputs/outputs. This can be useful to separate different groups of parameters, but also to define template-endpoints, which can then be further specialized. For example, we can define a base endpoint for our API, where all paths always start with `/api/v1.0`, and errors are always returned as a json: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* case class ErrorInfo(message: String) val baseEndpoint: PublicEndpoint[Unit, ErrorInfo, Unit, Any] = endpoint.in("api" / "v1.0").errorOut(jsonBody[ErrorInfo]) ``` Thanks to the fact that inputs/outputs accumulate, we can use the base endpoint to define more inputs, for example: ```scala case class Status(uptime: Long) val statusEndpoint: PublicEndpoint[Unit, ErrorInfo, Status, Any] = baseEndpoint.in("status").out(jsonBody[Status]) ``` The above endpoint will correspond to the `/api/v1.0/status` path. ## Mapping over input/output values Inputs/outputs can also be mapped over. As noted before, all mappings are bi-directional, so that they can be used both when interpreting an endpoint as a server, and as a client, as well as both in input and output contexts. There's a couple of ways to map over an input/output. First, there's the `map[II](f: I => II)(g: II => I)` method, which accepts functions which provide the mapping in both directions. For example: ```scala import sttp.tapir.* import java.util.UUID case class Paging(from: UUID, limit: Option[Int]) val paging: EndpointInput[Paging] = query[UUID]("start").and(query[Option[Int]]("limit")) .map(input => Paging(input._1, input._2))(paging => (paging.from, paging.limit)) ``` Next, you can use `mapDecode[II](f: I => DecodeResult[II])(g: II => I)`, to handle cases where decoding (mapping a low-level value to a higher-value one) can fail. There's a couple of failure reasons, captured by the alternatives of the `DecodeResult` trait. Mappings can also be done given a `Mapping[I, II]` instance. More on that in the section on [codecs](codecs.md). Creating a mapping between a tuple and a case class is a common operation, hence there's also a `mapTo[CaseClass]` method, which automatically provides the functions to construct/deconstruct the case class: ```scala val paging: EndpointInput[Paging] = query[UUID]("start").and(query[Option[Int]]("limit")) .mapTo[Paging] ``` Mapping methods can also be called on an endpoint (which is useful if inputs/outputs are accumulated, for example). The `Endpoint.mapIn`, `Endpoint.mapInTo` etc. have the same signatures are the ones above. ## Describing input/output values using annotations Inputs and outputs can also be built for case classes using annotations. For example, for the case class `User` ```scala import sttp.tapir.EndpointIO.annotations.* case class User( @query name: String, @cookie sessionId: Long ) ``` endpoint input can be generated using macro `EndpointInput.derived[User]` which is equivalent to ```scala import sttp.tapir.* val userInput: EndpointInput[User] = query[String]("user").and(cookie[Long]("sessionId")).mapTo[User] ``` Similarly, endpoint outputs can be derived using `EndpointOutput.derived[...]`. Following annotations are available in package `sttp.tapir.EndpointIO.annotations` for describing both input and output values: * `@header` captures a header with the same name as name of annotated field in a case class. This annotation can also be used with optional parameter `@header("headerName")` in order to capture a header with name `"headerName"` if a name of header is different from name of annotated field in a case class * `@headers` captures all headers. Can only be applied to fields represented as `List[Header]` * `@cookies` captures all cookies. Can only be applied to fields represented as `List[Cookie]` * `@jsonbody` captures JSON body of request or response. Can only be applied to field if there is implicit JSON `Codec` instance from `String` to target type * `@xmlbody` captures XML body of request or response. Also requires implicit XML `Codec` instance from `String` to target type Following annotations are only available for describing input values: * `@query` captures a query parameter with the same name as name of annotated field in a case class. The same as annotation `@header` it has optional parameter to specify alternative name for query parameter * `@params` captures all query parameters. Can only be applied to fields represented as `QueryParams` * `@cookie` captures a cookie with the same name as name of annotated field in a case class. The same as annotation `@header` it has optional parameter to specify alternative name for cookie * `@apikey` wraps any other input and designates it as an API key. Can only be used with another annotations * `@basic` extracts data from the `Authorization` header. Can only be applied for field represented as `UsernamePassword` * `@bearer` extracts data from the `Authorization` header removing the `Bearer` prefix. * `@path` captures a path segment. Can only be applied to field of a case class if this case class is annotated by annotation `@endpointInput`. For example, ```scala import sttp.tapir.EndpointIO.annotations.* @endpointInput("books/{year}/{genre}") case class Book( @path genre: String, @path year: Int, @query name: String ) ``` Annotation `@endpointInput` specifies endpoint path. In order to capture a segment of the path, it must be surrounded in curly braces. Following annotations are only available for describing output values: * `@setCookie` sends value in header `Set-Cookie`. The same as annotation `@header` it has optional parameter to specify alternative name for cookie. Can only be applied for field represented as `CookieValueWithMeta` * `@setCookies` sends several `Set-Cookie` headers. Can only be applied for field represented as `List[Cookie]` * `@statusCode` sets status code for response. Can only be applied for field represented as `StatusCode` ## Status codes ### Arbitrary status codes To provide a (varying) status code of a server response, use the `statusCode` output, which maps to a value of type `sttp.model.StatusCode`. In a server setting, the specific status code will then have to be provided dynamically by the server logic. The companion object contains known status codes as constants. This type of output is used only when interpreting the endpoint as a server. If your endpoint returns varying status codes which you would like to have listed in documentation use `statusCode.description(code1, "code1 description").description(code2, "code2 description")` output. ### Fixed status code A fixed status code can be specified using the `statusCode(code)` output. ### In server interpreters Unless specified otherwise, successful responses are returned with the `200 OK` status code, and errors with `400 Bad Request`. For exception and decode failure handling, see [error handling](../server/errors.md). ### Different outputs with different status codes If you'd like to return different content together with a varying status code, use a [oneOf](oneof.md) output. Each output variant can be paired with a fixed status code output (`statusCode(code)`), or a varying one, which will be determined dynamically by the server logic. ## Selected inputs/outputs for non-standard types * some header values can be decoded into a more structured representation, e.g. `header[MediaType]`, `header[ETag]`, `header[Range]`, `header[List[CacheDirective]]`, `header[List[Cookie]]`, `header[CookieWithMeta]` * the low-level body value can be tupled with the decoded high-level representation. This is useful e.g. if the hash of the body is required for security. A dedicated `jsonBodyWithRaw` description is available, but this can be used for any body e.g. `plainBody[(String, Int)]` * an input can be decoded into either one of two high-level values, e.g. `query[Either[String, Int]]("param")`. By default, such decoding is right-biased, so the right-hand codec is attempted fails, and only if it fails, the left-hand side codec is used ## Next Read on about [one-of mappings](oneof.md). # Working with JSON Json values are supported through codecs, which encode/decode values to json strings. Most often, you'll be using a third-party library to perform the actual json parsing/printing. See below for the list of supported libraries. All the integrations, when imported into scope, define `jsonBody[T]` and `jsonQuery[T]` methods. Instead of providing the json codec as an implicit value, this method depends on library-specific implicits being in scope, and basing on these values creates a json codec. The derivation also requires an implicit `Schema[T]` instance, which can be automatically derived. For more details see sections on [schema derivation](schemas.md) and on supporting [custom types](customtypes.md) in general. Such a design provides better error reporting, in case one of the components required to create the json codec is missing. ```{note} Note that the process of deriving schemas, and deriving library-specific json encoders and decoders is entirely separate (unless you're using the pickler module - see below). The first is controlled by tapir, the second - by the json library. Any customisation, e.g. for field naming or inheritance strategies, must be done separately for both derivations. ``` ## Pickler Alternatively, instead of deriving schemas and library-specific json encoders and decoders separately, you can use the experimental [pickler](pickler.md) module, which takes care of both derivation in a consistent way, which allows customization with a single, common configuration API. ## Implicit json codecs If you have a custom, implicit `Codec[String, T, Json]` instance, you should use the `customCodecJsonBody[T]` method instead. This description of endpoint input/output, instead of deriving a codec basing on other library-specific implicits, uses the json codec that is in scope. ## JSON as string If you'd like to work with JSON bodies in a serialised `String` form, instead of integrating on a higher level using one of the libraries mentioned below, you should use the `stringJsonBody` input/output. Note that in this case, the serialising/deserialising of the body must be part of the [server logic](../server/logic.md). A schema can be provided in this case as well: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* case class MyBody(field: Int) stringJsonBody.schema(implicitly[Schema[MyBody]].as[String]) ``` ## Circe To use [Circe](https://github.com/circe/circe), add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.13.31" ``` Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../other/mytapir.md)): ```scala import sttp.tapir.json.circe.* ``` The above import brings into scope the `jsonBody[T]` body input/output description, which creates a codec, given an in-scope circe `Encoder`/`Decoder` and a `Schema`. Circe includes a couple of approaches to generating encoders/decoders (manual, semi-auto and auto), so you may choose whatever suits you. Note that when using Circe's auto derivation, any encoders/decoders for custom types must be in scope as well. For example, to automatically generate a JSON codec for a case class: ```scala import sttp.tapir.* import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* import io.circe.generic.auto.* case class Book(author: String, title: String, year: Int) val bookInput: EndpointIO[Book] = jsonBody[Book] ``` ### Configuring the circe printer Circe lets you select an instance of `io.circe.Printer` to configure the way JSON objects are rendered. By default Tapir uses `Printer.nospaces`, which would render: ```scala import io.circe.* Json.obj( "key1" -> Json.fromString("present"), "key2" -> Json.Null ) ``` as ```json {"key1":"present","key2":null} ``` Suppose we would instead want to omit `null`-values from the object and pretty-print it. You can configure this by overriding the `jsonPrinter` in `tapir.circe.json.TapirJsonCirce`: ```scala import sttp.tapir.json.circe.* import io.circe.Printer object MyTapirJsonCirce extends TapirJsonCirce: override def jsonPrinter: Printer = Printer.spaces2.copy(dropNullValues = true) import MyTapirJsonCirce.* ``` Now the above JSON object will render as ```json {"key1":"present"} ``` ## µPickle To use [µPickle](http://www.lihaoyi.com/upickle/) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "1.13.31" ``` Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): ```scala import sttp.tapir.json.upickle.* ``` µPickle requires a `ReadWriter` in scope for each type you want to serialize. In order to provide one use the `macroRW` macro in the companion object as follows: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import upickle.default.* import sttp.tapir.json.upickle.* case class Book(author: String, title: String, year: Int) object Book: given ReadWriter[Book] = macroRW val bookInput: EndpointIO[Book] = jsonBody[Book] ``` Like Circe, µPickle allows you to control the rendered json output. Please see the [Custom Configuration](https://com-lihaoyi.github.io/upickle/#CustomConfiguration) of the manual for details. For more examples, including making a custom encoder/decoder, see [TapirJsonuPickleTests.scala](https://github.com/softwaremill/tapir/blob/master/json/upickle/src/test/scala/sttp/tapir/json/upickle/TapirJsonuPickleTests.scala) ## Play JSON To use [Play JSON](https://github.com/playframework/play-json) for **Play 3.0**, add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.13.31" ``` For **Play 2.9** use: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-play29" % "1.13.31" ``` Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): ```scala import sttp.tapir.json.play.* ``` Play JSON requires `Reads` and `Writes` implicit values in scope for each type you want to serialize. ## Spray JSON To use [Spray JSON](https://github.com/spray/spray-json) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "1.13.31" ``` Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): ```scala import sttp.tapir.json.spray.* ``` Spray JSON requires a `JsonFormat` implicit value in scope for each type you want to serialize. ## Tethys JSON To use [Tethys JSON](https://github.com/tethys-json/tethys) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "1.13.31" ``` Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): ```scala import sttp.tapir.json.tethys.* ``` Tethys JSON requires `JsonReader` and `JsonWriter` implicit values in scope for each type you want to serialize. ## Jsoniter Scala To use [Jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.13.31" ``` Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): ```scala import sttp.tapir.json.jsoniter.* ``` Jsoniter Scala requires `JsonValueCodec` implicit value in scope for each type you want to serialize. ## Json4s To use [json4s](https://github.com/json4s/json4s) add the following dependencies to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-json4s" % "1.13.31" ``` And one of the implementations: ```scala "org.json4s" %% "json4s-native" % "4.1.1" // Or "org.json4s" %% "json4s-jackson" % "4.1.1" ``` Next, import the package (or extend the `TapirJson4s` trait, see [MyTapir](../other/mytapir.md) and add `TapirJson4s` instead of `TapirCirceJson`): ```scala import sttp.tapir.json.json4s.* ``` Json4s requires `Serialization` and `Formats` implicit values in scope, for example: ```scala import org.json4s.* // ... given Serialization = org.json4s.jackson.Serialization given Formats = org.json4s.jackson.Serialization.formats(NoTypeHints) ``` ## Zio JSON To use [zio-json](https://github.com/zio/zio-json), add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "1.13.31" ``` Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): ```scala import sttp.tapir.json.zio.* ``` Zio JSON requires `JsonEncoder` and `JsonDecoder` implicit values in scope for each type you want to serialize. ## JSON query parameters You can specify query parameters in JSON format by using the `jsonQuery` method. For example, using Circe: ```scala import sttp.tapir.* import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* import io.circe.generic.auto.* case class Book(author: String, title: String, year: Int) val bookQuery: EndpointInput.Query[Book] = jsonQuery[Book]("book") ``` ## Other JSON libraries To add support for additional JSON libraries, see the [sources](https://github.com/softwaremill/tapir/blob/master/json/circe/src/main/scala/sttp/tapir/json/circe/TapirJsonCirce.scala) for the Circe codec (which is just a couple of lines of code). ## Coproducts (enums, sealed traits, classes) If you are serialising a sealed hierarchy, such as a Scala 3 `enum`, a `sealed trait` or `sealed class`, the configuration of [schema derivation](schemas.md) will have to match the configuration of your json library. Different json libraries have different defaults when it comes to a discrimination strategy, so in order to have the schemas (and hence the documentation) in sync with how the values are serialised, you will have to configure schema derivation as well. Schemas are referenced at the point of `jsonBody` and `jsonQuery` usage, so any configuration must be available in the implicit scope when these methods are called. ## Optional json bodies When the body is specified as an option, e.g. `jsonBody[Option[Book]]`, an empty body will be decoded as `None`. This is implemented by passing `null` to the json-library-specific decoder, when the schema specifies that the value is optional, and the body is empty. ## Next Read on about [working with forms](forms.md). # One-of variants There are two kind of one-of inputs/outputs: * `oneOf` outputs where the arbitrary-output variants can represent different content using different outputs, and * `oneOfBody` input/output where the body-only variants represent the same content, but with different content types ```{note} `oneOf` and `oneOfBody` outputs are not related to `oneOf:` schemas when [generating](../docs/openapi.md) OpenAPI documentation. Such schemas are generated for coproducts - e.g. `sealed trait` families - given an appropriate codec. See the documentation on [coproducts](schemas.md#sealed-traits--coproducts) for details. ``` ## `oneOf` outputs Outputs with multiple variants can be specified using the `oneOf` output. Each variant is defined using a one-of variant. All possible outputs must have a common supertype. Typically, the supertype is a sealed trait, and the variants are implementing case classes. Each one-of variant needs an `appliesTo` function to determine at run-time if the variant should be used for a given value. This function is inferred at compile time when using `oneOfVariant`, but can also be provided by hand, or if the compile-time inference fails, using one of the other factory methods (see below). A catch-all variant can be defined using `oneOfDefaultVariant`, and should be placed as the last variant in the list of possible variants. When encoding such an output to a response, the first matching output is chosen, using the following rules: 1. the variants `appliesTo` method, applied to the output value (as returned by the server logic) must return `true`. 2. when a fixed content type is specified by the output, it must match the request's `Accept` header (if present). This implements content negotiation. When decoding from a response, the first output which decodes successfully is chosen. The outputs might vary in status codes, headers (e.g. different content types), and body implementations. However, for bodies, only replayable ones can be used, and they need to have the same raw representation (e.g. all byte-array-base, or all file-based). Note that exhaustiveness of the variants (that all subtypes of `T` are covered) is not checked. For example, below is a specification for an endpoint where the error output is a sealed trait `ErrorInfo`; such a specification can then be refined and reused for other endpoints: ```scala import sttp.tapir.* import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* import sttp.model.StatusCode import io.circe.generic.auto.* sealed trait ErrorInfo case class NotFound(what: String) extends ErrorInfo case class Unauthorized(realm: String) extends ErrorInfo case class Unknown(code: Int, msg: String) extends ErrorInfo case object NoContent extends ErrorInfo // here we are defining an error output, but the same can be done for regular outputs val baseEndpoint = endpoint.errorOut( oneOf[ErrorInfo]( oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound].description("not found"))), oneOfVariant(statusCode(StatusCode.Unauthorized).and(jsonBody[Unauthorized].description("unauthorized"))), oneOfVariant(statusCode(StatusCode.NoContent).and(emptyOutputAs(NoContent))), oneOfDefaultVariant(jsonBody[Unknown].description("unknown")) ) ) ``` ### One-of-variant and type erasure Type erasure may prevent a one-of-variant from working properly. The following example will fail at compile time because `Right[NotFound]` and `Right[BadRequest]` will become `Right[Any]`: ```scala import sttp.tapir.* import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* import sttp.model.StatusCode import io.circe.generic.auto.* case class ServerError(what: String) sealed trait UserError case class BadRequest(what: String) extends UserError case class NotFound(what: String) extends UserError val baseEndpoint = endpoint.errorOut( oneOf[Either[ServerError, UserError]]( oneOfVariant(StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]].description("not found")), oneOfVariant(StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]].description("unauthorized")), oneOfVariant(StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description("unauthorized")), ) ) // error: // Type scala.util.Right[repl.MdocSession.MdocApp.ServerError, repl.MdocSession.MdocApp.NotFound], AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class util)),class Right),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class ServerError), TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class NotFound))) is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. // oneOfVariant(StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]].description("not found")), // ^ // error: // Type scala.util.Right[repl.MdocSession.MdocApp.ServerError, repl.MdocSession.MdocApp.BadRequest], AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class util)),class Right),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class ServerError), TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class BadRequest))) is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. // oneOfVariant(StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]].description("unauthorized")), // ^ // error: // Type scala.util.Left[repl.MdocSession.MdocApp.ServerError, repl.MdocSession.MdocApp.UserError], AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class util)),class Left),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class ServerError), TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),trait UserError))) is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. // oneOfVariant(StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description("unauthorized")), // ^ ``` The solution is therefore to handwrite a function checking that a value (of type `Any`) is of the correct type: ```scala val baseEndpoint = endpoint.errorOut( oneOf[Either[ServerError, UserError]]( oneOfVariantValueMatcher(StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]].description("not found")) { case Right(NotFound(_)) => true }, oneOfVariantValueMatcher(StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]].description("unauthorized")) { case Right(BadRequest(_)) => true }, oneOfVariantValueMatcher(StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description("unauthorized")) { case Left(ServerError(_)) => true } ) ) ``` Of course, you could use `oneOfVariantValueMatcher` to do runtime filtering for other purpose than solving type erasure. In the case of solving type erasure, writing by hand partial function to match value against composition of case class and sealed trait can be repetitive. To make that more easy, we provide the `MatchType` typeclass, so you can automatically derive that partial function: ```scala import sttp.tapir.typelevel.MatchType val baseEndpoint = endpoint.errorOut( oneOf[Either[ServerError, UserError]]( oneOfVariantFromMatchType(StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]].description("not found")), oneOfVariantFromMatchType(StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]].description("unauthorized")), oneOfVariantFromMatchType(StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description("unauthorized")) ) ) ``` ### One-of-variant and singleton types One-of variants can also be created so that they are used only for specific values. This is a specialisation of the `oneOfVariantValueMatcher` methods, which allows for a more convenient and compact description. There are two methods which allows working with multiple or single specific values: `oneOfVariantExactMatcher` and `oneOfVariantSingletonMatcher`. ### Error outputs Error outputs can be extended with new variants, which is especially useful for partial server endpoints, when the [security logic](../server/logic.md) is already provided. There are some specialised functions for this purpose. The `.errorOutVariant` and `.errorOutVariants` functions allow appending alternative error outputs; the result is typed as the common supertype of the existing and new outputs; hence usually this should be different from `Any`. At runtime, a class check is performed to choose the variant to use. The `.errorOutVariantPrepend` function allows prepending an error out variant, leaving the current error output as a default. This is useful e.g. when providing a more specific error output, than the current one. For example: ```scala import sttp.tapir.* trait DomainException { def help: String } case class SecurityException(help: String) extends DomainException case class LogicException(help: String) extends DomainException val base: PublicEndpoint[Unit, DomainException, Unit, Any] = endpoint .errorOut( oneOf( oneOfVariant(statusCode(StatusCode.BadRequest).and(stringBody.mapTo[LogicException])), oneOfDefaultVariant( statusCode(StatusCode.InternalServerError).and(stringBody.map(v => new DomainException { def help: String = v })(_.help)) ) ) ) val specialised: PublicEndpoint[Unit, DomainException, Unit, Any] = base .errorOutVariantPrepend(oneOfVariant(statusCode(StatusCode.Forbidden).and(stringBody.mapTo[SecurityException]))) ``` The `.errorOutEither` method allows adding an unrelated error output, at the cost of wrapping the result in an additional `Either`. ## `oneOfBody` inputs/outputs Input/output bodies which can be represented using different content types can be specified using `oneOfBody` inputs/outputs. Each body variant should represent the same content, and hence have the same high-level (decoded) type. To describe a body, which can be given as json, xml or plain text, create the following input/output description: ```scala import io.circe.generic.auto.* import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* case class User(name: String) implicit val userXmlCodec: Codec[String, User, CodecFormat.Xml] = Codec .id(CodecFormat.Xml(), Schema.string[String]) .mapDecode { xml => DecodeResult.fromOption("""(.*?)""".r.findFirstMatchIn(xml).map(_.group(1)).map(User)) }(user => s"${user.name}") .schema(implicitly[Schema[User]]) oneOfBody( jsonBody[User], xmlBody[User], stringBody.map(User(_))(_.name) ) ``` ## oneOf and non-blocking streaming [Streaming bodies](streaming.md) can't be used as normal inputs/outputs, as the streaming requirement needs to be propagated to the `Endpoint` type. This way, we assure at compile-time that only interpreters supporting the given streaming type can be used to interpret an endpoint. However, this makes it impossible to use streaming bodies in `oneOf` (via `oneOfVariant`) and `oneOfBody`, as both require normal input/outputs as parameters. To bypass this limitation, a `.toEndpointIO` method is available on streaming bodies, which "lifts" them to an `EndpointIO` type, forgetting the streaming requirement. This decreases type safety, as a run-time error might occur if an incompatible interpreter is used, however allows describing endpoints which require including streaming bodies in output variants. ```{note} If the same streaming body description is used in all branches of a `oneOf`, this can be refactored into a regular streaming body output + a varying set of output headers, expressed using `oneOf`. ``` ```{warning} Mixed streaming and non-streaming bodies defined as `oneOf` variants currently won't work with client interpreters. ``` ## Next Read on about [codecs](codecs.md). # JSON Pickler Pickler is an experimental module that simplifies working with JSON, using a consistent configuration API to provide both accurate endpoint documentation and server or client-side encoding/decoding. In [other](json.md) tapir-JSON integrations, you have to keep the `Schema` (which is used for documentation) in sync with the library-specific configuration of JSON encoders/decoders. The more customizations you need, like special field name encoding, or preferred way to represent sealed hierarchies, the more configuration you need to repeat (which is specific to the chosen library, like µPickle, Circe, etc.). `Pickler[T]` takes care of this, generating a consistent pair of `Schema[T]` and `JsonCodec[T]`, with single point of customization. Underneath it uses [µPickle](https://com-lihaoyi.github.io/upickle/) as its specific library for handling JSON, but it aims to keep it as an implementation detail. To use pickler, add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-pickler" % "1.13.31" ``` Please note that it is available only for Scala 3 and Scala.JS 3. ## Semi-automatic derivation A pickler can be derived directly using `Pickler.derived[T]`. This will derive both schema and `JsonCodec[T]`: ```scala import sttp.tapir.json.pickler.* import sttp.tapir.Codec.JsonCodec case class Book(author: String, title: String, year: Int) val pickler: Pickler[Book] = Pickler.derived val codec: JsonCodec[Book] = pickler.toCodec val bookJsonStr = // { "author": "Herman Melville", "title": Moby Dick", "year": 1851 } codec.encode(Book("Herman Melville", "Moby Dick", 1851)) ``` A `given` pickler in scope makes it available for `jsonQuery`, `jsonBody` and `jsonBodyWithRaw`, which need to be imported from the `sttp.tapir.json.pickler` package. For example: ```scala import sttp.tapir.* import sttp.tapir.json.pickler.* case class Book(author: String, title: String, year: Int) given Pickler[Book] = Pickler.derived val addBook: PublicEndpoint[Book, Unit, Unit, Any] = endpoint .in("books") .in("add") .in(jsonBody[Book].description("The book to add")) ``` A pickler also be derived using the `derives` keyword directly on a class: ```scala import sttp.tapir.json.pickler.* case class Book(author: String, title: String, year: Int) derives Pickler val pickler: Pickler[Book] = summon[Pickler[Book]] ``` Picklers for primitive types are available out-of-the-box. For more complex hierarchies, like nested `case class` structures or `enum`s, you'll need to provide picklers for all children (fields, enum cases etc.). Alternatively, you can use automatic derivation described below. ## Automatic derivation Picklers can be derived at usage side, when required, by adding the auto-derivation import: ```scala import sttp.tapir.json.pickler.* import sttp.tapir.json.pickler.generic.auto.* enum Country: case India case Bhutan case class Address(street: String, zipCode: String, country: Country) case class Person(name: String, address: Address) val pickler: Pickler[Person] = summon[Pickler[Person]] ``` However, this can negatively impact compilation performance, as the same pickler might be derived multiple times, for each usage of a type. This can be improved by explicitly providing picklers (as described in the semi-auto section above) either for all, or selected types. It's important then to make sure that the manually-provided picklers are in the implicit scope at the usage sites. ## Configuring pickler derivation It is possible to configure schema and codec derivation by providing an implicit `sttp.tapir.pickler.PicklerConfiguration`. This configuration allows switching field naming policy to `snake_case`, `kebab_case`, or an arbitrary transformation function, as well as setting the field name/value for the coproduct (sealed hierarchy) type discriminator, which is discussed in details in further sections. ```scala import sttp.tapir.json.pickler.PicklerConfiguration given customConfiguration: PicklerConfiguration = PicklerConfiguration .default .withSnakeCaseMemberNames ``` ## Enums / sealed traits / coproducts Pickler derivation for coproduct types (enums with parameters / sealed hierarchies) works automatically, by adding a `$type` discriminator field with the short class name. ```scala import sttp.tapir.json.pickler.PicklerConfiguration // encodes a case object as { "$type": "MyType" } given PicklerConfiguration = PicklerConfiguration.default ``` This behavior can be overridden either by changing the discriminator field name, or by using custom logic to get field value from base trait. Selaed hierarchies with all cases being objects are treated differently, considered as [enumerations](#enumerations). A discriminator field can be specified for coproducts by providing it in the configuration; this will be only used during automatic and semi-automatic derivation: ```scala import sttp.tapir.json.pickler.PicklerConfiguration // encodes a case object as { "who_am_i": "full.pkg.path.MyType" } given customConfiguration: PicklerConfiguration = PicklerConfiguration .default .withDiscriminator("who_am_i") .withFullDiscriminatorValues ``` The discriminator will be added as a field to all coproduct child codecs and schemas, if it’s not yet present. The schema of the added field will always be a Schema.string. Finally, the mapping between the discriminator field values and the child schemas will be generated using `Configuration.toDiscriminatorValue(childSchemaName)`. Finally, if the discriminator is a field that’s defined on the base trait (and hence in each implementation), the schemas can be specified as a custom implicit value using the `Pickler.oneOfUsingField` macro, for example (this will also generate the appropriate mappings): ```scala sealed trait Entity: def kind: String case class Person(firstName: String, lastName: String) extends Entity: def kind: String = "person" case class Organization(name: String) extends Entity: def kind: String = "org" import sttp.tapir.json.pickler.* val pPerson = Pickler.derived[Person] val pOrganization = Pickler.derived[Organization] given pEntity: Pickler[Entity] = Pickler.oneOfUsingField[Entity, String](_.kind, _.toString) ("person" -> pPerson, "org" -> pOrganization) // { "$type": "person", "firstName": "Jessica", "lastName": "West" } pEntity.toCodec.encode(Person("Jessica", "West")) ``` ## Customising derived schemas Schemas generated by picklers can be customized using annotations, just like with traditional schema derivation (see [here](schemas.md#using-annotations)). Some annotations automatically affect JSON codes: * `@encodedName` determines JSON field name * `@default` sets default value if the field is missing in JSON ## Enumerations Tapir schemas and JSON codecs treats following cases as "enumerations": 1. Scala 3 `enums`, where all cases are parameterless 2. Sealed hierarchies (coproducts), where all cases are case objects Such types are handled by `Pickler.derived[T]`: possible values are encoded as simple strings representing the case objects. For example: ```scala import sttp.tapir.json.pickler.* enum ColorEnum: case Green, Pink // or: // sealed trait ColorEnum // case object Green extends ColorEnum // case object Pink extends ColorEnum case class ColorResponse(color: ColorEnum, description: String) given Pickler[ColorEnum] = Pickler.derived val pResponse = Pickler.derived[ColorResponse] // { "color": "Pink", "description": "Pink desc" } pResponse.toCodec.encode( ColorResponse(ColorEnum.Pink, "Pink desc") ) // Enumeration schema with proper validator pResponse.schema ``` If sealed hierarchy or enum contain case classes with parameters, they are no longer an "enumeration", and will be treated as standard sealed hierarchies (coproducts): ```scala import sttp.tapir.json.pickler.* sealed trait ColorEnum case object Green extends ColorEnum case class Pink(intensity: Int) extends ColorEnum case class ColorResponse(color1: ColorEnum, color2: ColorEnum) given Pickler[ColorEnum] = Pickler.derived val pResponse = Pickler.derived[ColorResponse] // {"color1":{"$type":"Pink","intensity":85},"color2":{"$type":"Green"}} pResponse.toCodec.encode( ColorResponse(Pink(85), Green) ) ``` If you need to customize enumeration value encoding, use `Pickler.derivedEnumeration[T]`: ```scala import sttp.tapir.json.pickler.* enum ColorEnum: case Green, Pink case class ColorResponse(color: ColorEnum, description: String) given Pickler[ColorEnum] = Pickler .derivedEnumeration[ColorEnum] .customStringBased(_.ordinal.toString) val pResponse = Pickler.derived[ColorResponse] // { "color": "1", "description": "Pink desc" } pResponse.toCodec.encode( ColorResponse(ColorEnum.Pink, "Pink desc") ) // Enumeration schema with proper validator pResponse.schema ``` ## Using existing µPickle Readers and Writers If you have a case where you would like to use an existing custom `ReadWriter[T]`, you can still derive a `Pickler[T]`, but you have to provide both your `ReadWriter[T]` and a `Schema[T]` in the given (implicit) scope. With such a setup, you can proceed with `Pickler.derived[T]`. ## Divergences from default µPickle behavior * Tapir pickler serialises fields of type `Option[T]` as direct value `T` or skips serialisation if field value is `None`. This default behavior can be changed by setting `.withTransientNone(false)` in `PicklerConfiguration`, which would result in serialising `None` as `null`. This differs from uPickle, where optional values are wrapped in arrays. * Value classes (case classes extending AnyVal) will be serialised as simple values * Discriminator field value is a short class name, instead of full package with class name # Schema derivation A schema describes the shape of a value, how the low-level representation should be structured. Schemas are primarily used when generating [documentation](../docs/openapi.md) and when [validating](validation.md) incoming values. Schemas are typically defined as implicit values. They are part of [codecs](codecs.md), and are looked up in the implicit scope during codec derivation, as well as when using [json](json.md) or [form](forms.md) bodies. Implicit schemas for basic types (`String`, `Int`, etc.), and their collections (`Option`, `List`, `Array` etc.) are defined out-of-the box. They don't contain any meta-data, such as descriptions or example values. There's also a number of [datatype integrations](integrations.md) available, which provide schemas for various third-party libraries. For case classes and sealed hierarchies, `Schema[_]` values can be derived automatically using [Magnolia](https://github.com/softwaremill/magnolia), given that implicit schemas are available for all the case class's fields, or all of the implementations of the `enum`/`sealed trait`/`sealed class`. Two policies of custom type derivation are available: * automatic derivation * semi automatic derivation Finally, schemas can be provided by hand, e.g. for Java classes, or other custom types. As a fallback, you can also always use `Schema.string[T]` or `Schema.binary[T]`, however this will provide only basic documentation, and won't perform any [validation](validation.md). ## Automatic derivation Schemas for case classes, sealed traits and their children can be recursively derived. Importing `sttp.tapir.generic.auto.*` (or extending the `SchemaDerivation` trait) enables fully automatic derivation for `Schema`: ```scala import sttp.tapir.Schema import sttp.tapir.generic.auto.* case class Parent(child: Child) case class Child(value: String) // implicit schema used by codecs summon[Schema[Parent]] ``` If you have a case class which contains some non-standard types (other than strings, number, other case classes, collections), you only need to provide implicit schemas for them. Using these, the rest will be derived automatically. Note that when using [datatypes integrations](integrations.md), respective schemas & codecs must also be imported to enable the derivation, e.g. for [newtype](integrations.md#newtype-integration) you'll have to add `import sttp.tapir.codec.newtype.*` or extend `TapirCodecNewType`. ## Semi-automatic derivation Semi-automatic derivation can be done using `Schema.derived[T]`. It only derives selected type `T`. However, derivation is not recursive: schemas must be explicitly defined for every child type. This mode is easier to debug and helps to avoid issues encountered by automatic mode (wrong schemas for value classes or custom types): ```scala import sttp.tapir.Schema case class Parent(child: Child) case class Child(value: String) given Schema[Child] = Schema.derived given Schema[Parent] = Schema.derived ``` Note that while schemas for regular types can be safely defined as `val`s, in case of recursive values, the schema values must be `lazy val`s. ## Debugging schema derivation When deriving schemas using `Schema.derived[T]`, in case derivation fails, you'll get information for which part of `T` the schema cannot be found (e.g. a specific field, or a trait subtype). Given this diagnostic information you can drill down, and try to derive the schema (again using `Schema.derived`) for the problematic part. Eventually, you'll find the lowest-level type for which the schema cannot be derived. You might need to provide it manually, or use some kind of integration layer. This method may be used both with automatic and semi-automatic derivation. ## Scala3-specific derivation ### Derivation for recursive types In Scala3, any schemas for recursive types need to be provided as typed `implicit def` (not a `given`)! For example: ```scala case class RecursiveTest(data: List[RecursiveTest]) object RecursiveTest: implicit def f1Schema: Schema[RecursiveTest] = Schema.derived[RecursiveTest] ``` The implicit doesn't have to be defined in the companion object, just anywhere in scope. This applies to cases where the schema is looked up implicitly, e.g. for `jsonBody`. ### Derivation for union types Schemas for union types must be declared by hand, using the `Schema.derivedUnion[T]` method. Schemas for all components of the union type must be available in the implicit scope at the point of invocation. For example: ```scala val s: Schema[String | Int] = Schema.derivedUnion ``` If the union type is a named alias, the type needs to be provided explicitly, e.g.: ```scala type StringOrInt = String | Int val s: Schema[StringOrInt] = Schema.derivedUnion[StringOrInt] ``` If any of the components of the union type is a generic type, any of its validations will be skipped when validating the union type, as it's not possible to generate a runtime check for the generic type. ### Derivation for string-based constant union types e.g. `type AorB = "a" | "b"` See [enumerations](enumerations.md#scala-3-string-based-constant-union-types-to-enum) on how to use string-based unions of constant types as enums. ### Derivation for generic case classes Semi-automatic derivation with `derives Schema` or `given ... = Schema.derived` does not work well with generic case classes. For example, an application exposing a paginated REST API could use: ```scala final case class PaginatedBad[T](data: List[T], nextPage: Option[Int]) derives Schema final case class SomeInt(int: Int) derives Schema val nameBad = summon[Schema[PaginatedBad[SomeInt]]].name // nameBad: Option[SName] = Some( // SName( // fullName = "repl.MdocSession.MdocApp2.PaginatedBad", // typeParameterShortNames = List( // "repl.MdocSession.MdocApp2.PaginatedBad.derived$Schema.T" // ) // ) // ) ``` Due to the way semi-automatic derivation works, the name of `Schema[PaginatedBad[SomeInt]]` uses `T` instead of `SomeInt`. This leads to generating inconsistent OpenAPI specifications (as explained in [GitHub issues #3922](https://github.com/softwaremill/tapir/issues/3922) and [#4549](https://github.com/softwaremill/tapir/issues/4549)). To fix this, the `given` (or `implicit def`) statement can be made `inline`: ```scala final case class Paginated[T](data: List[T], nextPage: Option[Int]) object Paginated: inline given [T: Schema]: Schema[Paginated[T]] = Schema.derived val name = summon[Schema[Paginated[SomeInt]]].name // name: Option[SName] = Some( // SName( // fullName = "repl.MdocSession.MdocApp2.Paginated", // typeParameterShortNames = List("repl.MdocSession.MdocApp2.SomeInt") // ) // ) ``` If using `inline given` is not possible, or if the inline itself is part of a generic method, the name of the `Schema` can be adjusted after the derivation: ```scala final case class Paginated2[T](data: List[T], nextPage: Option[Int]) object Paginated2: given [T: Schema]: Schema[Paginated2[T]] = Schema.derivedWithTypeParameter // Or Schema.derivedWithTypeParameter[Paginated2, T] // Or Schema.derived[Paginated2[T]].renameWithTypeParameter[T] val name2 = summon[Schema[Paginated2[SomeInt]]].name // name2: Option[SName] = Some( // SName( // fullName = "repl.MdocSession.MdocApp2.Paginated2", // typeParameterShortNames = List("repl.MdocSession.MdocApp2.SomeInt") // ) // ) ``` ## Configuring derivation It is possible to configure Magnolia's automatic derivation to use `snake_case`, `kebab-case` or a custom field naming policy, by providing an implicit `sttp.tapir.generic.Configuration` value. This influences how the low-level representation is described in documentation: ```scala import sttp.tapir.generic.Configuration given Configuration = Configuration.default.withSnakeCaseMemberNames ``` ## Manually providing schemas Alternatively, `Schema[_]` values can be defined by hand, either for whole case classes, or only for some of its fields. For example, here we state that the schema for `MyCustomType` is a `String`: ```scala import sttp.tapir.* case class MyCustomType() given Schema[MyCustomType] = Schema.string // or, if the low-level representation is e.g. a number // given Schema[MyCustomType] = Schema(SchemaType.SInteger()) ``` ## Sealed traits / coproducts Schema derivation for coproduct types (sealed hierarchies) is supported as well. By default, such hierarchies will be represented as a coproduct which contains a list of child schemas, without any discriminators. ```{note} Note that whichever approach you choose to define the coproduct schema, it has to match the way the value is encoded and decoded by the codec. E.g. when the schema is for a json body, the discriminator must be separately configured in the json library, matching the configuration of the schema. Alternatively, instead of deriving schemas and json codecs separately, you can use the experimental [pickler](pickler.md) module, which provides a higher level `Pickler` concept, which takes care of consistent derivation. ``` ### Field discriminators A discriminator field can be specified for coproducts by providing it in the configuration; this will be only used during automatic and semi-automatic derivation: ```scala import sttp.tapir.generic.Configuration given Configuration = Configuration.default.withDiscriminator("who_am_i") ``` The discriminator will be added as a field to all coproduct child schemas, if it's not yet present. The schema of the added field will always be a `Schema.string`. Finally, the mapping between the discriminator field values and the child schemas will be generated using `Configuration.toDiscriminatorValue(childSchemaName)`. Alternatively, derived schemas can be customised (see also below), and a discriminator can be added by calling the `SchemaType.SCoproduct.addDiscriminatorField(name, schema, maping)` method. This method is useful when using semi-automatic or automatic derivation; in both cases a custom implicit has to be defined, basing on the derived one: ```scala import sttp.tapir.* import sttp.tapir.generic.Derived import sttp.tapir.generic.auto.* sealed trait MyCoproduct case class Child1(s: String) extends MyCoproduct // ... implementations of MyCoproduct ... given Schema[MyCoproduct] = val derived = implicitly[Derived[Schema[MyCoproduct]]].value derived.schemaType match case s: SchemaType.SCoproduct[_] => derived.copy(schemaType = s.addDiscriminatorField( FieldName("myField"), Schema.string, Map( "value1" -> SchemaType.SRef(Schema.SName("com.myproject.Child1")), // ... other mappings ... ) )) case _ => ??? ``` Finally, if the discriminator is a field that's defined on the base trait (and hence in each implementation), the schemas can be specified as a custom implicit value using the `Schema.oneOfUsingField` macro, for example (this will also generate the appropriate mappings): ```scala sealed trait Entity: def kind: String case class Person(firstName: String, lastName: String) extends Entity: def kind: String = "person" case class Organization(name: String) extends Entity: def kind: String = "org" import sttp.tapir.* val sPerson = Schema.derived[Person] val sOrganization = Schema.derived[Organization] given Schema[Entity] = Schema.oneOfUsingField[Entity, String](_.kind, _.toString)( "person" -> sPerson, "org" -> sOrganization) ``` ### Wrapper object discriminators Another discrimination strategy uses a wrapper object. Such an object contains a single field, with its name corresponding to the discriminator value. A schema can be automatically generated using the `Schema.oneOfWrapped` macro, for example: ```scala sealed trait Entity case class Person(firstName: String, lastName: String) extends Entity case class Organization(name: String) extends Entity import sttp.tapir.* import sttp.tapir.generic.auto.* // to derive child schemas given Schema[Entity] = Schema.oneOfWrapped[Entity] ``` The names of the field in the wrapper object will be generated using the implicit `Configuration`. If for some reason this is insufficient, you can generate schemas for individual wrapper objects using `Schema.wrapWithSingleFieldProduct`. ## Customising derived schemas ### Using annotations In some cases, it might be desirable to customise the derived schemas, e.g. to add a description to a particular field of a case class. One way the automatic & semi-automatic derivation can be customised is using annotations: * `@encodedName` sets name for case class's field which is used in the encoded form (and also in documentation) * `@description` sets description for the whole case class or its field * `@default` sets default value for a case class field (plus an optional encoded form used in documentation) * `@encodedExample` sets example value for a case class field which is used in the documentation in the encoded form * `@format` sets the format for a case class field * `@deprecated` marks a case class's field as deprecated * `@validate` will add the given validator to a case class field * `@validateEach` will add the given validator to the elements of a case class field. Useful for validating the value contained in an `Option` (when it's defined), and collection elements These annotations will adjust schemas, after they are looked up using the normal implicit mechanisms. ### Using implicits If the target type isn't accessible or can't be modified, schemas can be customized by looking up an implicit instance of the `Derived[Schema[T]]` type, modifying the value, and assigning it to an implicit schema. When such an implicit `Schema[T]` is in scope will have higher priority than the built-in low-priority conversion from `Derived[Schema[T]]` to `Schema[T]`. Schemas for products/coproducts (case classes and case class families) can be traversed and modified using `.modify` method. To traverse collections or options, use `.each`. For example: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.generic.Derived case class Basket(fruits: List[FruitAmount]) case class FruitAmount(fruit: String, amount: Int) given Schema[Basket] = summon[Derived[Schema[Basket]]].value .modify(_.fruits.each.amount)(_.description("How many fruits?")) ``` There is also an unsafe variant of this method, but it should be avoided in most cases. The "unsafe" prefix comes from the fact that the method takes a list of strings, which represent fields, and the correctness of this specification is not checked. Non-standard collections can be unwrapped in the modification path by providing an implicit value of `ModifyFunctor`. ### Using value classes/tagged types An alternative to customising schemas for case class fields of primitive type (e.g. `Int`s), is creating a unique type. As schema lookup is type-driven, if a schema for a such type is provided as an implicit value, it will be used during automatic or semi-automatic schema derivation. Such schemas can have custom meta-data, including description, validation, etc. To introduce unique types for primitive values, which don't have a runtime overhead, you can use value classes or [type tagging](https://github.com/softwaremill/scala-common#tagging). For example, to support an integer wrapped in a value type in a json body, we need to provide Circe encoders and decoders (if that's the json library that we are using), schema information with validator: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.{Encoder, Decoder} import io.circe.generic.semiauto.* case class Amount(v: Int) extends AnyVal case class FruitAmount(fruit: String, amount: Amount) given Schema[Amount] = Schema(SchemaType.SInteger()).validate(Validator.min(1).contramap(_.v)) given Encoder[Amount] = Encoder.encodeInt.contramap(_.v) given Decoder[Amount] = Decoder.decodeInt.map(Amount.apply) given Decoder[FruitAmount] = deriveDecoder[FruitAmount] given Encoder[FruitAmount] = deriveEncoder[FruitAmount] val e: PublicEndpoint[FruitAmount, Unit, Unit, Nothing] = endpoint.in(jsonBody[FruitAmount]) ``` ## Next Read on about [enumerations](enumerations.md). # Security Endpoints can have dedicated security-related inputs. The type of those inputs is captured as the first type parameter, `A`, of the `Endpoint` type. Security inputs can be added by `.securityIn` methods, and behave the same as regular inputs. The security inputs play a crucial role when defining the [server logic](../server/logic.md) for an endpoint. They also aim to clearly communicate which part of the endpoint's input is security-related. Finally, they are part of a mechanism for building reusable base "secure" endpoints, either with the security logic provided or not, which can later be extended by other endpoints. ## Authentication inputs Inputs which map to authentication credentials can be created using methods available in the `auth` object. Such inputs in addition to the base input (such as an `Authorization` header or a cookie), contain security-related metadata, for example the name of the security scheme that should be used for documentation. ```{note} Note that security inputs added using `.securityIn` can contain both dedicated auth credentials inputs created using one of the methods from `auth`, and arbitrary "regular" inputs, such as path components. Similarly, regular inputs can contain inputs created through `auth`, though typically this shouldn't be the case. ``` Currently, the following authentication inputs are available (assuming `import sttp.tapir.*`): * `auth.apiKey(anotherInput)`: wraps any other input and designates it as an api key. The input is typically a header, cookie or a query parameter * `auth.basic[T]`: reads data from the `Authorization` header, removing the `Basic ` prefix. To parse the data as a base64-encoded username/password combination, use: `basic[UsernamePassword]`. * `auth.bearer[T]`: reads data from the `Authorization` header, removing the `Bearer ` prefix. To get the token as a string, use: `bearer[String]`. * `auth.oauth2.authorizationCodeFlow(authorizationUrl, scopes, tokenUrl, refreshUrl): EndpointInput[String]`: creates an OAuth2 authorization using authorization code - sign in using an auth service (for documentation, requires defining also the `oauth2-redirect.html`, see [Generating OpenAPI documentation](../docs/openapi.md)). Other OAuth2 flows are also supported, as well as optional variants: `authorizationCodeFlow[Optional]`, `clientCredentialsFlow[Optional]`, `implicitFlow[Optional]`. ## Authentication challenges For each `auth` scheme, one can define `WWW-Authenticate` headers that should be returned by the server in case input is not provided. Default behavior is to return `401` status with the headers needed for authentication. To return different status codes when authentication is missing, the decode failure handler can [be customised](../server/errors.md). For example, if you define `endpoint.get.securityIn("path").securityIn(auth.basic[UsernamePassword]())` then the browser will show you a password prompt. ## Grouping authentication inputs in docs Optional and multiple authentication inputs have some additional rules as to how hey map to documentation, see the ["Authentication inputs and security requirements"](../docs/openapi.md) section in the OpenAPI docs for details. ## Limiting request body length *Unsupported backends*: This feature is available for all server backends *except*: `akka-grpc`, `Armeria`, `Finatra`, `Helidon Nima`, `pekko-grpc`. Individual endpoints can be annotated with content length limit: ```scala import sttp.tapir.* import sttp.tapir.server.model.EndpointExtensions.* val limitedEndpoint = endpoint.maxRequestBodyLength(maxBytes = 16384L) ``` The `EndpointsExtensions` utility is available in `tapir-server` core module. Such protection prevents loading all the input data if it exceeds the limit. Instead, it will result in a `HTTP 413` response to the client. Please note that in case of endpoints with `streamBody` input type, the server logic receives a reference to a lazily evaluated stream, so actual length verification will happen only when the logic performs streams processing, not earlier. ## Next Read on about [streaming support](streaming.md). # Serving static content Tapir contains predefined endpoints, server logic and server endpoints which allow serving static content, originating from local files or application resources. These endpoints respect etags, byte ranges as well as if-modified-since headers. ```{note} Since Tapir 1.3.0, static content is supported via the new `tapir-files` module. If you're looking for the API documentation of the old static content API, switch documentation to an older version. ``` In order to use static content endpoints, add the module to your dependencies: ```scala "com.softwaremill.sttp.tapir" %% "tapir-files" % "1.13.31" ``` ## Files The easiest way to expose static content from the local filesystem is to use the `staticFilesServerEndpoint`. This method is parametrised with the path, at which the content should be exposed, as well as the local system path, from which to read the data. Such an endpoint has to be interpreted using your server interpreter. For example, using the [netty-sync](../server/netty.md) interpreter: ```scala import sttp.tapir.* import sttp.tapir.files.* import sttp.tapir.server.netty.sync.NettySyncServer NettySyncServer() .addEndpoint(staticFilesGetServerEndpoint("site" / "static")("/home/static/data")) .startAndWait() ``` Using the above endpoint, a request to `/site/static/css/styles.css` will try to read the `/home/static/data/css/styles.css` file. To expose files without a prefix, use `emptyInput`. For example, below exposes the content of `/var/www` at `http://localhost:8080`: ```scala import sttp.tapir.server.netty.sync.NettySyncServer import sttp.tapir.emptyInput import sttp.tapir.* import sttp.tapir.files.* NettySyncServer() .addEndpoint(staticFilesGetServerEndpoint(emptyInput)("/var/www")) .startAndWait() ``` A single file can be exposed using `staticFileGetServerEndpoint`. Similarly, you can expose HEAD endpoints with `staticFileHeadServerEndpoint` and `staticFilesHeadServerEndpoint`. If you want to serve both GET and HEAD, use `staticFilesServerEndpoints`. The file server endpoints can be secured using `ServerLogic.prependSecurity`, see [server logic](../server/logic.md) for details. ## Resources Similarly, the `staticResourcesGetServerEndpoint` can be used to expose the application's resources at the given prefix. A single resource can be exposed using `staticResourceGetServerEndpoint`. ## Additional Configuration ### FileOptions Endpoint constructor methods for files and resources can receive optional `FileOptions`, which allow to configure additional settings: ```scala import sttp.model.headers.ETag import sttp.tapir.emptyInput import sttp.tapir.* import sttp.tapir.files.* import sttp.shared.Identity import java.net.URL val customETag: Option[RangeValue] => URL => Option[ETag] = ??? val customFileFilter: List[String] => Boolean = ??? val options: FilesOptions[Identity] = FilesOptions .default[Identity] // serves file.txt.gz instead of file.txt if available and Accept-Encoding contains "gzip" .withUseGzippedIfAvailable .calculateETag(customETag) .fileFilter(customFileFilter) .defaultFile(List("default.md")) val endpoint = staticFilesGetServerEndpoint[Identity](emptyInput)("/var/www", options) ``` ### Static Headers In addition to the `FileOptions`, zero or more `Header`s can also be provided to a server endpoint as a `List[Header]`. These can be used to attached fixed headers to the responses. As an example: ```scala import sttp.model.Header import sttp.model.headers.CacheDirective import sttp.tapir.emptyInput import sttp.tapir.* import sttp.tapir.files.* import sttp.shared.Identity import scala.concurrent.duration.FiniteDuration import java.util.concurrent.TimeUnit val headers = List(Header.cacheControl(CacheDirective.MaxAge(FiniteDuration(365, TimeUnit.DAYS)))) val endpoint = staticFilesGetServerEndpoint[Identity](emptyInput)("/var/www", extraHeaders = headers) ``` The above `cacheControl` header makes this resource eligible for caching for up to a year on the client machine. The headers which can be provided are not limited to just caching, however. ## Endpoint description and server logic The descriptions of endpoints which should serve static data, and the server logic which implements the actual file/resource reading are also available separately for further customisation. The `staticFilesGetEndpoint` and `staticResourcesGetEndpoint` are descriptions which contain the metadata (including caching headers) required to serve a file or resource, and possible error outcomes. This is captured using the `StaticInput`, `StaticErrorOuput` and `StaticOutput[T]` classes. The `sttp.tapir.files.Files` and `sttp.tapir.files.Resources` objects contain the logic implementing server-side reading of files or resources, with etag/last modification support. ## WebJars The content of [WebJars](https://www.webjars.org) that are available on the classpath can be exposed using the following routes (here using the `/resources` context path): ```scala import sttp.tapir.* import sttp.tapir.files.* import sttp.shared.Identity val webJarRoutes = staticResourcesGetServerEndpoint[Identity]("resources")( this.getClass.getClassLoader, "META-INF/resources/webjars") ``` # Streaming support Both input and output bodies can be mapped to a stream, by using `stream[*]Body(streams)`. The parameter `streams` must implement the `Streams[S]` capability, and determines the precise type of the binary stream supported by the given non-blocking streams implementation. The interpreter must then support the given capability. Refer to the documentation of server/client interpreters for more information. ```{note} Here, streams refer to asynchronous, non-blocking, "reactive" stream implementations, such as [akka-streams](https://doc.akka.io/docs/akka/current/stream/index.html), [fs2](https://fs2.io) or [zio-streams](https://zio.dev/docs/datatypes/datatypes_stream). If you'd like to use blocking streams (such as `InputStream`), these are available through e.g. `inputStreamBody` without any additional requirements on the interpreter. ``` Adding a stream body input/output influences both the type of the input/output, as well as the 5th type parameter of `Endpoint`, which specifies the requirements regarding supported stream types for interpreters. When using a stream body, a schema must be provided for documentation. By default, when using `streamBinaryBody`, the schema will simply be that of a binary body. If you have a textual stream, you can use `streamTextBody`. In that case, you'll also need to provide the default format (media type) and optional charset to be used to determine the content type. To provide an arbitrary schema, use `streamBody`. Note, however, that this schema will only be used for generating documentation. The incoming stream data will not be validated using the schema validators. For example, to specify that the output is an akka-stream, which is a (presumably large) serialised list of json objects mapping to the `Person` class: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.capabilities.pekko.PekkoStreams case class Person(name: String) // copying the derived json schema type endpoint.out(streamBody(PekkoStreams)(Schema.derived[List[Person]], CodecFormat.Json())) ``` See also the [runnable streaming examples](../examples.md). ## Next Read on about [web sockets](websockets.md). # Validation Tapir supports validation for primitive types. Validation of composite values, whole data structures, business rules enforcement etc. should be done as part of the [server logic](../server/logic.md) of the endpoint, using the dedicated error output (the `E` in `Endpoint[A, I, E, O, S]`) to report errors. For some guidelines as to where to perform a specific type of validation, see the ["Validation analysis paralysis"](https://blog.softwaremill.com/validation-analysis-paralysis-ca9bdef0a6d7) article. A good indicator as to where to place particular validation logic might be if the property that we are checking is a format error, or a business-level error? The validation capabilities described in this section are intended only for format errors. Validation rules added using the built-in validators are translated to [OpenAPI](../docs/openapi.md) documentation. ## Adding validators to schemas A validator is always part of a `Schema`, which is part of a `Codec`. It can validate either the top-level object, or some nested component (such as a field value). If you are using automatic or semi-automatic schema derivation, validators for such schemas, and their nested components, including collections and options, can be added as described in [schema customisation](schemas.md#customising-derived-schemas). ## Adding validators to inputs/outputs Validators can also be added to individual inputs/outputs. Behind the scenes, this modifies the schema, but it's easier to add top-level validators this way, rather than modifying the implicit schemas, for example: ```scala import sttp.tapir.* val e = endpoint.in( query[Int]("amount") .validate(Validator.min(0)) .validate(Validator.max(100))) ``` For optional/iterable inputs/outputs, to validate the contained value(s), use: ```scala import sttp.tapir.* query[Option[Int]]("item").validateOption(Validator.min(0)) query[List[Int]]("item").validateIterable(Validator.min(0)) // validates each repeated parameter ``` ## Adding validators to codecs Finally, if you are creating a reusable [codec](codecs.md), a validator can be added to it as well: ```scala import sttp.tapir.* import sttp.tapir.CodecFormat.TextPlain case class MyId(id: String) given Codec[String, MyId, TextPlain] = Codec.string .map(MyId(_))(_.id) .validate(Validator.pattern("^[A-Z].*").contramap(_.id)) ``` ## Decode failures The validators are run when a value is being decoded from its low-level representation. This is done using the `Codec.decode` method, which returns a `DecodeResult`. Such a result can be successful, or a decoding failure. Keep in mind that the validator mechanism described here is meant for input/output values which are in an incorrect low-level format. Validation and more generally decoding failures should be reported only for format failures. Business validation errors, which are often contextual, should use the error output instead. To customise error messages that are returned upon validation/decode failures by the server, see [error handling](../server/errors.md). ## Enumeration validators Validators for enumerations can be created using: * for arbitrary types, using `Validator.enumeration`, which takes the list of possible values * for `sealed` hierarchies, where all implementations are objects, using `Validator.derivedEnumeration[T]`. This method is a macro which determines the possible values. * for Scala3 `enum`s, where all implementation don't have parameters, using `Validator.derivedEnumeration[T]` as above * for Scala2 `Enumeration#Value`, automatically derived `Schema`s have the validator added (see `Schema.derivedEnumerationValue`) The enumeration schemas and codecs that are created by tapir-provided methods already have the enumeration validator added. See the section on [enumerations](enumerations.md) for more information. ### Enumeration values in documentation To properly represent possible values in documentation, the enum validator additionally needs an `encode` method, which converts the enum value to a raw type (typically a string). This can be specified by: * explicitly providing it using the overloaded `enumeration` method with an `encode` parameter * by using one of the `.encode` methods on the `Validator.Enumeration` instance * when the values possible values are of a basic type (numbers, strings), the encode function is inferred if not present * by adding the validator directly to a codec using `.validate` (the encode function is then taken from the codec) For example: ```scala import sttp.tapir.* sealed trait Color case object Blue extends Color case object Red extends Color // providing the enum values by hand given Schema[Color] = Schema.string.validate( Validator.enumeration(List(Blue, Red), (c: Color) => Some(c.toString.toLowerCase))) ``` ## Validation of unrepresentable values Note that validation is run on a fully decoded values. That is, during decoding, first all the provided decoding functions are run, followed by validations. If you'd like to validate before decoding, e.g. because the value isn't representable unless validator conditions are met due to preconditions, you can use ``.mapValidate``. However, this will cause the validator function to be run twice if there are no validation error. ## Next Read on about [content types](contenttype.md). # Web sockets Web sockets are supported through stream pipes, converting a stream of incoming messages to a stream of outgoing messages. That's why web socket endpoints require both the `Streams` and `WebSocket` capabilities (see [streaming support](streaming.md) for more information on streams). ## Typed web sockets Web sockets outputs can be used in two variants. In the first, both requests and responses are handled by [codecs](codecs.md). Typically, a codec handles either text or binary messages, signalling decode failure (which closes the web socket), if an unsupported frame is passed to decoding. For example, here's an endpoint where the requests are strings (hence only text frames are handled), and the responses are parsed/formatted as json: ```scala import sttp.tapir.* import sttp.capabilities.pekko.PekkoStreams import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* import io.circe.generic.auto.* case class Response(msg: String, count: Int) endpoint.out( webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](PekkoStreams)) ``` When creating a `webSocketBody`, we need to provide the following parameters: * the type or requests, along with its codec format (which is used to lookup the appropriate codec, as well as determines the media type in documentation) * the type of responses, along with its codec format * the `Streams` implementation, which determines the pipe type By default, ping-pong frames are handled automatically, fragmented frames are combined, and close frames aren't decoded, but this can be customized through methods on `webSocketBody`. ## Close frames If you are using the default codecs between `WebSocketFrame` and your high-level types, and you'd like to either be notified that a websocket has been closed by the client, or close it from the server side, then you should wrap your high-level type into an `Option`. The default codecs map close frames to `None`, and regular (decoded text/binary) frames to `Some`. Hence, using the following definition: ```scala webSocketBody[Option[String], CodecFormat.TextPlain, Option[Response], CodecFormat.Json](PekkoStreams) ``` the websocket-processing pipe will receive a `None: Option[String]` when the client closes the web socket. Moreover, if the pipe emits a `None: Option[Response]`, the web socket will be closed by the server. Alternatively, if the codec for your high-level type already handles close frames (but its schema is not derived as optional), you can request that the close frames are decoded by the codec as well. Here's an example which does this on the server side: ```scala webSocketBody[...](...).decodeCloseRequests(true) ``` If you'd like to decode close frames when the endpoint is interpreted as a client, you should use the `decodeCloseResponses` method. ```{note} Not all server interpreters expose control frames (such as close frames) to user (and Tapir) code. Refer to the documentation of individual interpreters for more details. ``` ## Raw web sockets The second web socket handling variant is to obtain a raw pipe transforming `WebSocketFrame`s: ```scala import org.apache.pekko.stream.scaladsl.Flow import sttp.tapir.* import sttp.capabilities.pekko.PekkoStreams import sttp.capabilities.WebSockets import sttp.ws.WebSocketFrame endpoint.out(webSocketBodyRaw(PekkoStreams)): PublicEndpoint[ Unit, Unit, Flow[WebSocketFrame, WebSocketFrame, Any], PekkoStreams with WebSockets] ``` Such a pipe by default doesn't handle ping-pong frames automatically, doesn't concatenate fragmented flames, and passes close frames to the pipe as well. As before, this can be customized by methods on the returned output. Request/response schemas can be customized through `.requestsSchema` and `.responsesSchema`. ## Interpreting as a server When interpreting a web socket endpoint as a server, the [server logic](../server/logic.md) needs to provide a streaming-specific pipe from requests to responses. E.g. in Pekko's case, this will be `Flow[REQ, RESP, Any]`. Refer to the documentation of interpreters for more details, as not all interpreters support all settings. ## Interpreting as a client When interpreting a web socket endpoint as a client, after applying the input parameters, the result is a pipe representing message processing as it happens on the server. Refer to the documentation of interpreters for more details, as there are interpreter-specific additional requirements. ## Interpreting as documentation Web socket endpoints can be interpreted into [AsyncAPI documentation](../docs/asyncapi.md). ## Determining if the request is a web socket upgrade The `isWebSocket` endpoint input can be used to determine if the request contains the web socket upgrade headers. The input only impacts server interpreters, doesn't affect documentation and its value is discarded by client interpreters. ## Next Read on about [datatypes integrations](integrations.md). # Working with XML Enabling support for XML is a matter of implementing proper [`XmlCodec[T]`](codecs.md) and providing it in scope. This enables encoding objects to XML strings, and decoding XML strings to objects. Implementation is fairly easy, and for now, one guide on how to integrate with scalaxb is provided. ```{note} Note, that implementing `XmlCodec[T]` would require deriving not only XML library encoders/decoders, but also tapir related `Schema[T]`. These are completely separate - any customization e.g. for field naming or inheritance strategies must be done separately for both derivations. For more details see sections on [schema derivation](schemas.md) and on supporting [custom types](customtypes.md) in general. ``` ## Scalaxb If you possess the XML Schema definition file (`.xsd` file) consider using the scalaxb tool, which generates needed models and serialization/deserialization logic. To use the tool please follow the documentation on [setting up](https://scalaxb.org/setup) and [running](https://scalaxb.org/running-scalaxb) scalaxb. After code generation, create the `TapirXmlScalaxb` trait (or trait with another name of your choosing) and add the following code snippet: ```scala import generated.`package`.defaultScope // import may differ depending on location of generated code import scalaxb.XMLFormat // import may differ depending on location of generated code import scalaxb.`package`.{fromXML, toXML} // import may differ depending on location of generated code import sttp.tapir.Codec.XmlCodec import sttp.tapir.DecodeResult.{Error, Value} import sttp.tapir.{Codec, EndpointIO, Schema, stringBodyUtf8AnyFormat} import scala.xml.{NodeSeq, XML} trait TapirXmlScalaxb: case class XmlElementLabel(label: String) def xmlBody[T: XMLFormat: Schema](implicit l: XmlElementLabel): EndpointIO.Body[String, T] = stringBodyUtf8AnyFormat(scalaxbCodec[T]) given (using XmlFormat[T], Schema[T], XmlElementLabel): XmlCodec[T] = Codec.xml((s: String) => try Value(fromXML[T](XML.loadString(s))) catch case e: Exception => Error(s, e) )((t: T) => { val nodeSeq: NodeSeq = toXML[T](obj = t, elementLabel = summon[XmlElementLabel].label, scope = defaultScope) nodeSeq.toString() }) ``` This creates `XmlCodec[T]` that would encode / decode the types with `XMLFormat`, `Schema` and with `XmlElementLabel` provided in scope. It also introduces `xmlBody` helper method, which allows you to easily express, that the declared endpoint consumes or returns XML. Next to this trait, you might want to introduce `xml` package object to simplify imports. ```scala package object xml extends TapirXmlScalaxb ``` From now on, XML serialization/deserialization would work for all classes generated from `.xsd` file as long as `XMLFormat`, `Schema` and `XmlElementLabel` would be implicitly provided in the scope. `XMLFormat` is scalaxb related, allowing for XML encoding / decoding. [`Schema`](schemas.md) is tapir related, used primarily when generating documentation and validating incoming values. And `XmlElementLabel` is required by scalaxb code when encoding to XML to give proper top node name. Usage example: ```scala import sttp.tapir.{PublicEndpoint, endpoint} import cats.effect.IO import generated.Outer // import may differ depending on location of generated code import sttp.tapir.generic.auto.* // needed for Schema derivation import sttp.tapir.server.ServerEndpoint object Endpoints: import xml.* // imports tapir related serialization / deserialization logic given XmlElementLabel = XmlElementLabel("outer") // `label` is needed by scalaxb code to properly encode the top node of the xml val xmlEndpoint: PublicEndpoint[Outer, Unit, Outer, Any] = endpoint.post // `Outer` is a class generated by scalaxb based on .xsd file. .in("xml") .in(xmlBody[Outer]) .out(xmlBody[Outer]) ``` If the generation of OpenAPI documentation is required, consider adding OpenAPI doc extension on schema providing XML namespace as described in the "Prefixes and Namespaces" section at [OpenAPI documentation regarding handling XML](https://swagger.io/docs/specification/data-models/representing-xml/). This would add `xmlns` property to example request/responses at swagger, which is required by scalaxb to properly deserialize XML. For more information on adding OpenAPI doc extension in tapir refer to [documentation](../docs/openapi.md#openapi-specification-extensions). Adding xml namespace doc extension to `Outer`'s `Schema` example: ```scala case class XmlNamespace(namespace: String) given Schema[Outer] = summon[Derived[Schema[Outer]]].value .docsExtension("xml", XmlNamespace("http://www.example.com/innerouter")) ``` Also, you might want to check the repository with [example project](https://github.com/softwaremill/tapir-scalaxb-example) showcasing integration with tapir and scalaxb. # Overview of server integrations Server interpreters require the endpoint descriptions to be combined with "business logic": functions, which compute an endpoint's output parameters based on input parameters. Tapir integrates with a number of HTTP server implementations, through **server interpreters**. We recommend starting with the Netty-based server. However, if you already have experience with another server, or are using one in your project already, just continue doing so, and enjoy seamless Tapir integration! Currently supported: * [Netty](netty.md) (using direct-style, `Future`s, cats-effect or ZIO) * [Http4s](http4s.md) `HttpRoutes[F]` (using cats-effect or [ZIO](zio-http4s.md)) * [Pekko HTTP](pekkohttp.md) `Route`s/`Directive`s * [Akka HTTP](akkahttp.md) `Route`s/`Directive`s * [Vert.X](vertx.md) `Router => Route` (using `Future`s, cats-effect or ZIO) * [Armeria](armeria.md) `HttpServiceWithRoutes` (using `Future`s, cats-effect or ZIO) * [ZIO Http](ziohttp.md) `Http` * [Play](play.md) `Route` * [Helidon Níma](nima.md) (using JVM 21 Virtual Threads and direct style) * [Finatra](finatra.md) `http.Controller` * [JDK HTTP](jdkhttp.md) `HttpHandler` (simple, synchronous API only) * [aws](aws.md) through Lambda/SAM/Terraform * [gRPC](../other/grpc.md) # Running as an akka-http server To expose an endpoint as an [akka-http](https://doc.akka.io/docs/akka-http/current/) server, first add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.13.31" ``` This will transitively pull 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: ```scala "com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.13.31" exclude("com.typesafe.akka", "akka-stream_2.12") ``` Now import the object: ```scala import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter ``` ## Using `toRoute` The `toRoute` method requires a single, or a list of `ServerEndpoint`s, which can be created by adding [server logic](logic.md) to an endpoint. For example: ```scala import sttp.tapir._ import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter import scala.concurrent.Future import akka.http.scaladsl.server.Route import scala.concurrent.ExecutionContext.Implicits.global def countCharacters(s: String): Future[Either[Unit, Int]] = Future.successful(Right[Unit, Int](s.length)) val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) val countCharactersRoute: Route = AkkaHttpServerInterpreter().toRoute(countCharactersEndpoint.serverLogic(countCharacters)) ``` ## Combining directives The tapir-generated `Route` captures from the request only what is described by the endpoint. Combine with other akka-http directives to add additional behavior, or get more information from the request. For example, wrap the tapir-generated route in a metrics route, or nest a security directive in the tapir-generated directive. Edge-case endpoints, which require special logic not expressible using tapir, can be implemented directly using akka-http. For example: ```scala import sttp.tapir._ import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter import akka.http.scaladsl.server._ import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global class Special def metricsDirective: Directive0 = ??? def specialDirective: Directive1[Special] = ??? val tapirEndpoint: PublicEndpoint[String, Unit, Unit, Any] = endpoint.in(path[String]("input")) val myRoute: Route = metricsDirective { specialDirective { special => AkkaHttpServerInterpreter().toRoute(tapirEndpoint.serverLogic[Future] { input => ??? /* here we can use both `special` and `input` values */ }) } } ``` ## Streaming The akka-http interpreter accepts streaming bodies of type `Source[ByteString, Any]`, as described by the `AkkaStreams` capability. Both response bodies and request bodies can be streamed. Usage: `streamBody(AkkaStreams)(schema, format)`. The capability can be added to the classpath independently of the interpreter through the `"com.softwaremill.sttp.shared" %% "akka"` dependency. ## Web sockets The interpreter supports web sockets, with pipes of type `Flow[REQ, RESP, Any]`. See [web sockets](../endpoint/websockets.md) for more details. akka-http does not expose control frames (`Ping`, `Pong` and `Close`), so any setting regarding them are discarded, and ping/pong frames which are sent explicitly are ignored. [Automatic pings](https://doc.akka.io/docs/akka-http/current/server-side/websocket-support.html#automatic-keep-alive-ping-support) can be instead enabled through configuration. ## Server Sent Events The interpreter supports [SSE (Server Sent Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). For example, to define an endpoint that returns event stream: ```scala import akka.stream.scaladsl.Source import sttp.model.sse.ServerSentEvent import sttp.tapir._ import sttp.tapir.server.akkahttp.{AkkaHttpServerInterpreter, serverSentEventsBody} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global val sseEndpoint = endpoint.get.out(serverSentEventsBody) val routes = AkkaHttpServerInterpreter().toRoute(sseEndpoint.serverLogicSuccess[Future](_ => Future.successful(Source.single(ServerSentEvent(Some("data"), None, None, None))) )) ``` ## Configuration The interpreter can be configured by providing an `AkkaHttpServerOptions` value, see [server options](options.md) for details. # Running as an Armeria server Endpoints can be mounted as `TapirService[S, F]` on top of [Armeria](https://armeria.dev)'s `HttpServiceWithRoutes`. Armeria interpreter can be used with different effect systems (cats-effect, ZIO) as well as Scala's standard `Future`. ## Scala's standard `Future` Add the following dependency ```scala "com.softwaremill.sttp.tapir" %% "tapir-armeria-server" % "1.13.31" ``` and import the object: ```scala import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter ``` to use this interpreter with `Future`. The `toService` method require a single, or a list of `ServerEndpoint`s, which can be created by adding [server logic](logic.md) to an endpoint. ```scala import sttp.tapir.* import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter import scala.concurrent.Future import com.linecorp.armeria.server.Server // JVM entry point that starts the HTTP server - uncommment @main to run /* @main */ def armeriaSerer(): Unit = val tapirEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? // your definition here def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? // your logic here val tapirService = ArmeriaFutureServerInterpreter().toService(tapirEndpoint.serverLogic((logic _).tupled)) val server = Server .builder() .service(tapirService) // your endpoint is bound to the server .build() server.start().join() ``` This interpreter also supports streaming using Armeria Streams which is fully compatible with Reactive Streams: ```scala import sttp.capabilities.armeria.ArmeriaStreams import sttp.tapir.* import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter import scala.concurrent.Future import com.linecorp.armeria.common.HttpData import com.linecorp.armeria.common.stream.StreamMessage import org.reactivestreams.Publisher val streamingResponse: PublicEndpoint[Int, Unit, Publisher[HttpData], ArmeriaStreams] = endpoint .in("stream") .in(query[Int]("key")) .out(streamTextBody(ArmeriaStreams)(CodecFormat.TextPlain())) def streamLogic(foo: Int): Future[Publisher[HttpData]] = Future.successful(StreamMessage.of(HttpData.ofUtf8("hello"), HttpData.ofUtf8("world"))) val tapirService = ArmeriaFutureServerInterpreter().toService(streamingResponse.serverLogicSuccess(streamLogic)) ``` ## Configuration Every endpoint can be configured by providing an instance of `ArmeriaFutureEndpointOptions`, see [server options](options.md) for details. Note that Armeria automatically injects an `ExecutionContext` on top of Armeria's `EventLoop` to invoke the logic. ## Cats Effect Add the following dependency ```scala "com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % "1.13.31" ``` to use this interpreter with Cats Effect typeclasses. Then import the object: ```scala import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter ``` This object contains the `toService(e: ServerEndpoint[Fs2Streams[F], F])` method which returns a `TapirService[Fs2Streams[F], F]`. An HTTP server can then be started as in the following example: ```scala import sttp.tapir.* import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter import cats.effect.* import cats.effect.std.Dispatcher import com.linecorp.armeria.server.Server import java.util.concurrent.CompletableFuture object Main extends IOApp: override def run(args: List[String]): IO[ExitCode] = val tapirEndpoint: PublicEndpoint[String, Unit, String, Any] = ??? def logic(req: String): IO[Either[Unit, String]] = ??? Dispatcher[IO] .flatMap { dispatcher => Resource .make( IO.async_[Server] { cb => val tapirService = ArmeriaCatsServerInterpreter[IO](dispatcher).toService(tapirEndpoint.serverLogic(logic)) val server = Server .builder() .service(tapirService) .build() server.start().handle[Unit] { case (_, null) => cb(Right(server)) case (_, cause) => cb(Left(cause)) } } )({ server => IO.fromCompletableFuture(IO(server.closeAsync().asInstanceOf[CompletableFuture[Unit]])) }) } .use(_ => IO.never) ``` This interpreter also supports streaming using FS2 streams: ```scala import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.* import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter import cats.effect.* import cats.effect.std.Dispatcher import fs2.* val streamingResponse: Endpoint[Unit, Int, Unit, Stream[IO, Byte], Fs2Streams[IO]] = endpoint .in("stream") .in(query[Int]("times")) .out(streamTextBody(Fs2Streams[IO])(CodecFormat.TextPlain())) def streamLogic(times: Int): IO[Stream[IO, Byte]] = IO.pure(Stream.chunk(Chunk.array("Hello world!".getBytes)).repeatN(times)) def dispatcher: Dispatcher[IO] = ??? val tapirService = ArmeriaCatsServerInterpreter(dispatcher).toService(streamingResponse.serverLogicSuccess(streamLogic)) ``` ## ZIO Add the following dependency ```scala "com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "1.13.31" ``` to use this interpreter with ZIO. Then import the object: ```scala import sttp.tapir.server.armeria.zio.ArmeriaZioServerInterpreter ``` This object contains `toService(e: ServerEndpoint[ZioStreams, RIO[R, *]])` method which returns a `TapirService[ZioStreams, RIO[R, *]]`. An HTTP server can then be started as in the following example: ```scala import com.linecorp.armeria.server.Server import sttp.tapir.* import sttp.tapir.server.armeria.zio.ArmeriaZioServerInterpreter import sttp.tapir.ztapir.* import zio.{ExitCode, Runtime, UIO, URIO, ZIO, ZIOAppDefault} import java.util.concurrent.CompletableFuture object Main extends ZIOAppDefault: override def run: URIO[Any, ExitCode] = given Runtime[Any] = Runtime.default val tapirEndpoint: PublicEndpoint[String, Unit, String, Any] = ??? def logic(key: String): UIO[String] = ??? val s = ZIO.fromCompletableFuture { val tapirService = ArmeriaZioServerInterpreter().toService(tapirEndpoint.zServerLogic(logic)) val server = Server .builder() .service(tapirService) .build() server.start().thenApply[Server](_ => server) } ZIO.scoped(ZIO.acquireRelease(s)(server => ZIO.fromCompletableFuture(server.closeAsync().asInstanceOf[CompletableFuture[Unit]]).orDie) *> ZIO.never).exitCode ``` This interpreter supports streaming using ZStreams. # Running using the AWS serverless stack Tapir server endpoints can be packaged and deployed as an [AWS Lambda](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) function. You can utilize a single lambda function for multiple endpoints ("Fat Lambda"), or deploy the same jar multiple times, so that each handles its own endpoint or subset of endpoints. To invoke the function, HTTP requests can be proxied through [AWS API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html). To configure API Gateway routes, and the Lambda function, tools like [AWS SAM](https://aws.amazon.com/serverless/sam/), [AWS CDK](https://aws.amazon.com/cdk/) or [Terraform](https://www.terraform.io/) can be used, to automate cloud deployments. For an overview of how this works in more detail, see [this blog post](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396). ## Runtimes & interpreters AWS Lambda supports several [runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) — the language-specific environment in which your function code executes. Tapir supports three of them, each described in its own section below. Note: the handler type parameter (`AwsRequest` or `AwsRequestV1`) determines the expected API Gateway request format. Use `AwsRequest` for API Gateway V2 (HTTP API) and `AwsRequestV1` for API Gateway V1 (REST API). V1 requests are automatically normalized to V2 internally. ### Java runtime With the Java runtime, you package your code as a fat JAR (via `assembly`) and upload it to AWS, which manages the JVM. The Lambda entry point implements the AWS [RequestStreamHandler](https://github.com/aws/aws-lambda-java-libs/blob/master/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java) interface, which is provided by the `aws-lambda-java-runtime-interface-client` dependency (pulled in transitively by all tapir AWS Lambda modules). You can use this with direct-style, cats-effect, or ZIO: #### Direct-style No effect library needed. Extend `SyncLambdaHandler`, provide your endpoints via `getAllEndpoints`, and you're done — the class directly implements `RequestStreamHandler`. Uses `AwsSyncServerInterpreter` (`AwsRequest => AwsResponse`). ```scala "com.softwaremill.sttp.tapir" %% "tapir-aws-lambda-core" % "1.13.31" ``` Example: [SyncLambdaApiExample](https://github.com/softwaremill/tapir/blob/master/serverless/aws/examples/src/main/scalajvm/sttp/tapir/serverless/aws/examples/SyncLambdaApiExample.scala) #### cats-effect Extend `LambdaHandler[F, R]`, provide your endpoints via `getAllEndpoints`, and implement `handleRequest` by calling `process(input, output).unsafeRunSync()`. Uses `AwsCatsEffectServerInterpreter` (`AwsRequest => F[AwsResponse]`). ```scala "com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.13.31" ``` Examples: [LambdaApiExample](https://github.com/softwaremill/tapir/blob/master/serverless/aws/examples/src/main/scalajvm/sttp/tapir/serverless/aws/examples/LambdaApiExample.scala), [V1 variant](https://github.com/softwaremill/tapir/blob/master/serverless/aws/examples/src/main/scalajvm/sttp/tapir/serverless/aws/examples/LambdaApiV1Example.scala) #### ZIO Create a handler instance via `ZioLambdaHandler.default(endpoints)`, then call `handler.process[AwsRequest](input, output)` from a `RequestStreamHandler` implementation, running the ZIO effect with `Runtime.default.unsafe.run(...)`. Uses `AwsZioServerInterpreter` (`AwsRequest => RIO[Env, AwsResponse]`). ```scala "com.softwaremill.sttp.tapir" %% "tapir-aws-lambda-zio" % "1.13.31" ``` Example: [ZioLambdaHandlerImpl](https://github.com/softwaremill/tapir/blob/master/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/ZioLambdaHandlerImpl.scala) ### Custom runtime As an alternative to the AWS-provided Java runtime, you can use a [custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html) where your application includes its own runtime loop that polls the Lambda Runtime API for invocations via HTTP. This can be useful when running in custom containers or with GraalVM native images. The tradeoff is an additional dependency on an HTTP client (sttp client4 with the fs2 backend). #### cats-effect Extend `AwsLambdaIORuntime` and provide your `endpoints`. The base class implements `main` and runs the polling loop via `AwsLambdaRuntime`. Uses `AwsCatsEffectServerInterpreter` (`AwsRequest => F[AwsResponse]`). ```scala "com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.13.31" ``` ### NodeJS runtime You can also compile your Scala code to JavaScript via Scala.js and run it on Node.js. The main benefit are faster cold starts, though with AWS's SnapStart, this might or might not be a significant advantage. Handler functions are exported to JavaScript using `@JSExportTopLevel` and return a `js.Promise[AwsJsResponse]`. Use `AwsJsRouteHandler` to bridge between the JS request/response types and tapir's `Route[F]`. You can use this with either Future or cats-effect: #### Future Uses `AwsFutureServerInterpreter` (`AwsRequest => Future[AwsResponse]`). ```scala "com.softwaremill.sttp.tapir" %%% "tapir-aws-lambda-core" % "1.13.31" ``` Example: [LambdaApiJsExample](https://github.com/softwaremill/tapir/blob/master/serverless/aws/examples/src/main/scalajs/sttp/tapir/serverless/aws/examples/LambdaApiJsExample.scala) #### cats-effect Uses `AwsCatsEffectServerInterpreter` (`AwsRequest => IO[AwsResponse]`). Supports both plain `Route[IO]` (via `catsIOHandler`) and `Resource[IO, Route[IO]]` (via `catsResourceHandler`). ```scala "com.softwaremill.sttp.tapir" %%% "tapir-aws-lambda" % "1.13.31" ``` Example: [LambdaApiJsResourceExample](https://github.com/softwaremill/tapir/blob/master/serverless/aws/examples/src/main/scalajs/sttp/tapir/serverless/aws/examples/LambdaApiJsResourceExample.scala) ## Deployment To make it possible, to call your endpoints, you will need to deploy your application to Lambda, and configure Amazon API Gateway. Tapir leverages ways of doing it provided by AWS, you can choose from: AWS SAM template file, terraform configuration, and AWS CDK. You can start by adding one of the following dependencies to your project, and then follow examples: ```scala "com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.13.31" "com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.13.31" "com.softwaremill.sttp.tapir" %% "tapir-aws-cdk" % "1.13.31" ``` ### Examples Go ahead and clone tapir project. To deploy you application to AWS you will need to have an AWS account and [AWS command line tools installed](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html). #### SAM SAM can be deployed using Java runtime or NodeJS runtime. For each of these cases first you will have to install [AWS SAM command line tool](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-command-reference.html), and create a S3 bucket, that will be used during deployment. Before going further, open sbt shell, as it will be needed for both runtimes. For Java runtime, use sbt to run `assembly` task, and then `runMain sttp.tapir.serverless.aws.examples.SamTemplateExample`, this will generate `template.yaml` sam file in main directory For NodeJS runtime, first generate AWS Lambda yaml file by execution inside sbt shell command `awsExamples/runMain sttp.tapir.serverless.aws.examples.SamJsTemplateExample`, and then build Node.js module with `awsExamplesJS/fastLinkJS`, it will create all-in-one JS file under `tapir/serverless/aws/examples/target/js-2.13/tapir-aws-examples-fastopt/main.js` From now the steps for both runtimes are the same: 1. Before deploying, if you want to test your application locally, you will need Docker. Execute `sam local start-api --warm-containers EAGER`, there will be a link displayed at the console output 2. To deploy it to AWS, run `sam deploy --template-file template.yaml --stack-name sam-app --capabilities CAPABILITY_IAM --s3-bucket [name of your bucket]`. The console output should print url of the application, just add `/api/hello` to the end of it, and you should see `Hello!` message. Be aware in case of Java runtime, the first call can take a little longer as the application takes some time to start, but consecutive calls will be much faster. 3. When you want to rollback changes made on AWS, run `sam delete --stack-name sam-app` #### Terraform Terraform deployment requires you to have a S3 bucket. 1. Install [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) 2. Run `assembly` task inside sbt shell 3. Open a terminal in `tapir/serverless/aws/examples/target/jvm-2.13` directory. That's where the fat jar is saved. You need to upload it into your s3 bucket. Using command line tools: `aws s3 cp tapir-aws-examples.jar s3://{your-bucket}/{your-key}`. 4. Run `runMain sttp.tapir.serverless.aws.examples.TerraformConfigExample {your-aws-region} {your-bucket} {your-key}` inside sbt shell 5. Open terminal in tapir root directory, run `terraform init` and `terraform apply` That will create `api_gateway.tf.json` configuration and deploy Api Gateway and lambda function to AWS. Terraform will output the url of the created API Gateway which you can call followed by `/api/hello` path. To destroy all the created resources run `terraform destroy`. #### CDK 1. First you need to install: * [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) * [AWS CDK Toolkit](https://docs.aws.amazon.com/cdk/v2/guide/cli.html) 2. Open sbt shell, then run `assembly` task, and execute `runMain sttp.tapir.serverless.aws.examples.CdkAppExample` to generate CDK application template under `cdk` directory 3. Go to `cdk` and run `npm install`, it will create all files needed for the deployment 4. Before deploying, if you want to test your application locally, you will need Docker and [AWS SAM command line tool](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-command-reference.html) , then execute `cdk synth`, and `sam local start-api -t cdk.out/TapirCdkStack.template.json --warm-containers EAGER` 5. To deploy it to AWS simply run `cdk deploy` 6. When you want to rollback changes made on AWS, run `cdk destroy` # Logging & debugging When dealing with multiple endpoints, how to find out which endpoint handled a request, or why an endpoint didn't handle a request? For this purpose, tapir provides optional logging. The logging options (and messages) can be customised by providing an instance of the `ServerLog` trait, which is part of [server options](options.md). An instance of the default implementation, `DefaultServerLog`, is available in the companion object for the interpreter's options class, e.g. `Http4sServerOptions.defaultServerLog` or `NettyZioServerOptions.defaultServerLog`. This instance can be customised using the following flags: 1. `logWhenReceived`: log when a request is first received (default: `false`, `DEBUG` log) 2. `logWhenHandled`: log when a request is handled by an endpoint, or when the inputs can't be decoded, and the decode failure maps to a response (default: `true`, `DEBUG` log) 3. `logAllDecodeFailures`: log each time when the inputs can't be decoded, and the decode failure doesn't map to a response (the next endpoint will be tried; default: `false`, `DEBUG` log) 4. `logLogicExceptions`: log when there's an exception during evaluation of the server logic (default: `true`, `ERROR` log) Logging all decode failures (3) might be helpful when debugging, but can also produce a large amount of logs, hence it's disabled by default. Additionally, you can customize the `DefaultServerLog` with `.ignoreEndpoints` to exclude some endpoints from logging with `logWhenHandled`, but not from logging exceptions and decoding failures. Even if logging for a particular category (as described above) is set to `true`, normal logger rules apply - if you don't see the logs, please verify your logging levels for the appropriate packages. # Error handling Error handling in tapir is divided into three areas: 1. Error outputs: defined per-endpoint, used for errors handled by the business logic 2. Failed effects: exceptions which are not handled by the server logic (corresponds to 5xx responses) 3. Decode failures: format errors, when the input values can't be decoded (corresponds to 4xx responses, or trying another endpoint) While 1. is specific to an endpoint, handlers for 2. and 3. are typically the same for multiple endpoints, and are specified as part of the server's interpreter [options](options.md). ## Error outputs Each endpoint can contain dedicated error outputs, in addition to outputs which are used in case of success. The business logic can then return either an error value, or a success value. Any business-logic-level errors should be signalled this way. This can include validation, failure of downstream services, or inability to serve the request at that time. If the business logic signals errors as exceptions, some or all can be recovered from and mapped to an error value. For example: ```scala import sttp.tapir.* import sttp.tapir.server.netty.NettyFutureServerInterpreter import scala.concurrent.{ExecutionContext, Future} import scala.util.* given ExecutionContext = ExecutionContext.global type ErrorInfo = String def logic(s: String): Future[Int] = ??? def handleErrors[T](f: Future[T]): Future[Either[ErrorInfo, T]] = f.transform { case Success(v) => Success(Right(v)) case Failure(e) => println(s"Exception when running endpoint logic: $e") Success(Left(e.getMessage)) } NettyFutureServerInterpreter().toRoute( endpoint .errorOut(plainBody[ErrorInfo]) .out(plainBody[Int]) .in(query[String]("name")) .serverLogic((logic _).andThen(handleErrors)) ) ``` In the above example, errors are represented as `String`s (aliased to `ErrorInfo` for readability). When the logic completes successfully an `Int` is returned. Any exceptions that are raised are logged, and represented as a value of type `ErrorInfo`. Following the convention, the left side of the `Either[ErrorInfo, T]` represents an error, and the right side success. Alternatively, errors can be recovered from failed effects and mapped to the error output - provided that the `E` type in the endpoint description is itself a subclass of exception. This can be done by using the `serverLogicRecoverErrors` to specify the server logic, see the dedicated [section](logic.md) for more information. ## Failed effects: unhandled exceptions If the logic function, which is passed to the server interpreter, fails (i.e. throws an exception, which results in a failed `Future` or `IO`/`Task`), this will be handled by the logging and exception interceptors. By default, an `ERROR` will be logged, and an `500 InternalServerError` returned. ## Decode failures Quite often user input will be malformed and decoding of the request will fail. Should the request be completed with a `400 Bad Request` response, or should the request be forwarded to another endpoint? By default, tapir follows OpenAPI conventions, that an endpoint is uniquely identified by the method and served path. That's why: - a `405 Method Not Allowed` is returned if multiple endpoints have been interpreted, and for at least one of them the path matched, but the method didn't. We assume that all endpoints for that path have been given to the interpreter, hence the response. This behavior can be [customised or turned off](#custom-reject-interceptor) using the `RejectInterceptor` - an "endpoint doesn't match" result is returned if the request method or path doesn't match. The http library should attempt to serve this request with the next endpoint. The path doesn't match if a path segment is missing, there's a constant value mismatch or a decoding error (e.g. parsing a segment to an `Int` fails) - if an [authentication input](../endpoint/security.md) fails to decode, a `401 Unauthorized` is returned together with an appropriate `WWW-Authenticate` header. See [options](options.md) documentation on how to return a `404` instead, to hide the endpoint - otherwise, we assume that this is the correct endpoint to serve the request, but the parameters are somehow malformed. A `400 Bad Request` response is returned if a query parameter, header or body causes any decode failure, or if the decoding a path capture causes a validation error. It is also the default result on path segment decoding failures, for example if parsing such a segment to an `Int` fails, or there's an incorrect Enumeratum Enum value passed. The behavior described in the latter three points can be customised by providing a custom `sttp.tapir.server.interceptor.decodefailure.DecodeFailureHandler` when creating the server options. This handler, basing on the request, failing input and failure description can decide, whether to return a "no match" or a specific response. Only the first failure encountered for a specific endpoint is passed to the `DecodeFailureHandler`. Inputs are decoded in the following order: method, path, query, header, body. Note that the decode failure handler is used **only** for failures that occur during decoding of path, query, body and header parameters - while invoking `Codec.decode`. It does not handle any failures or exceptions that occur when invoking the logic of the endpoint. ### Custom reject interceptor The default reject interceptor can be customised by providing your own reject handler - a case class consisting of: - a function to determine the response format (other than the default plain text), - a default status code and message to be used if the rejected input was not the HTTP method. Two default implementations of the reject handler are provided by the `DefaultRejectHandler`: - `DefaultRejectHandler` - which returns a `405 Method Not Allowed` when the HTTP method was rejected, and otherwise propagates the rejection to the server interpreter library, - `DefaultRejectHandler.orNotFound` - similar, but returns a `404 Not Found` instead of propagating when the rejected input was not the HTTP method. ### Default failure handler The default decode failure handler is a case class, consisting of functions which decide whether to respond with an error or return a "no match", create error messages and create the response. Parts of the default behavior can be swapped, e.g. to return responses in a different format (other than plain text), or customise the error messages. Moreover, when using the `DefaultDecodeFailureHandler`, decode failure handling can be overriden on a per-input/output basis, by setting an attribute. For example: ```scala import sttp.tapir.* // bringing into scope the onDecodeFailureNextEndpoint extension method import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.OnDecodeFailure.* case class UserId(value: String) object UserId: given Codec[String, UserId, CodecFormat.TextPlain] = Codec.string.mapDecode(raw => UserId.make(raw) match { case Left(error) => DecodeResult.Error(raw, new IllegalArgumentException(s"Invalid User value ($raw), failed with $error")) case Right(result) => DecodeResult.Value(result) })(_.value) def make(in: String): Either[String, UserId] = if (in.length > 5) Right(new UserId(in)) else Left("Too short") // If your codec for UserId fails, allow checking other endpoints for possible matches, like /customer/some_special_case endpoint.in("customer" / path[UserId]("user_id").onDecodeFailureNextEndpoint) // Another endpoint, tried after matching for the previous one fails on decoding of a UserId endpoint.in("customer" / "some_special_case") ``` ## Customising how error messages are rendered To return error responses in a different format (other than plain text), you can customise both the exception, decode failure and reject handlers individually, or use the `CustomiseInterceptors.defaultHandlers` method which customises the default ones for you. We'll need to provide both the endpoint output which should be used for error messages, along with the output's value: ```scala import sttp.tapir.* import sttp.tapir.server.model.ValuedEndpointOutput import sttp.tapir.server.netty.NettyFutureServerOptions import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* case class MyFailure(msg: String) def myFailureResponse(m: String): ValuedEndpointOutput[_] = ValuedEndpointOutput(jsonBody[MyFailure], MyFailure(m)) val myServerOptions: NettyFutureServerOptions = NettyFutureServerOptions .customiseInterceptors .defaultHandlers(myFailureResponse) .options ``` If you want to customise anything beyond the rendering of the error message, or use non-default implementations of the exception handler / decode failure handler, you'll still have to customise each by hand. ## Endpoints which cannot fail In some cases, you can have endpoints which "cannot fail", that is which do not have any "expected" errors. For these scenarios, you can use `infallibleEndpoint` as a starting point (instead of `endpoint`). When using `infallibleEndpoint`, the error type is fixed to `Nothing`. Of course, the server logic for such endpoints might still throw exceptions / return failed effects. Such failures are logged & intercepted in the usual way, as described above. # Running as a Finatra server To expose an endpoint as an [finatra](https://twitter.github.io/finatra/) server, first add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-finatra-server" % "1.13.31" ``` and import the object: ```scala import sttp.tapir.server.finatra.FinatraServerInterpreter ``` This interpreter supports the twitter `Future`. Or, if you would like to use cats-effect project, you can add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-finatra-server-cats" % "1.13.31" ``` and import the object: ```scala import sttp.tapir.server.finatra.cats.FinatraCatsServerInterpreter ``` This interpreter supports any effect that implements cats-effect `Effect` type. The `toRoute` method on the interpreter requires a `ServerEndpoint`, which can be created by adding [server logic](logic.md) to any endpoint. For example: ```scala import sttp.tapir._ import sttp.tapir.server.finatra.{ FinatraServerInterpreter, FinatraRoute } import com.twitter.util.Future def countCharacters(s: String): Future[Either[Unit, Int]] = Future.value(Right[Unit, Int](s.length)) val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) val countCharactersRoute: FinatraRoute = FinatraServerInterpreter().toRoute(countCharactersEndpoint.serverLogic(countCharacters)) ``` or a cats-effect's example: ```scala import cats.effect.IO import cats.effect.std.Dispatcher import sttp.tapir._ import sttp.tapir.server.finatra.FinatraRoute import sttp.tapir.server.finatra.cats.FinatraCatsServerInterpreter def countCharacters(s: String): IO[Either[Unit, Int]] = IO.pure(Right[Unit, Int](s.length)) val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) def dispatcher: Dispatcher[IO] = ??? val countCharactersRoute: FinatraRoute = FinatraCatsServerInterpreter(dispatcher).toRoute(countCharactersEndpoint.serverLogic(countCharacters)) ``` Now that you've created the `FinatraRoute`, add `TapirController` as a trait to your `Controller`. You can then add the created route with `addTapirRoute`. ```scala import sttp.tapir.server.finatra._ import com.twitter.finatra.http.Controller val aRoute: FinatraRoute = ??? class MyController extends Controller with TapirController { addTapirRoute(aRoute) } ``` # Running as an http4s server To expose an endpoint as an [http4s](https://http4s.org) server, first add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.13.31" ``` and import the object: ```scala import sttp.tapir.server.http4s.Http4sServerInterpreter ``` The `toRoutes` and `toHttp` methods require a single, or a list of `ServerEndpoint`s, which can be created by adding [server logic](logic.md) to an endpoint. The server logic should use a cats-effect-support `F[_]` effect type. For example: ```scala import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter import cats.effect.IO import org.http4s.HttpRoutes def countCharacters(s: String): IO[Either[Unit, Int]] = IO.pure(Right[Unit, Int](s.length)) val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) val countCharactersRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(countCharactersEndpoint.serverLogic(countCharacters _)) ``` The created `HttpRoutes` are the usual http4s `Kleisli`-based transformation of a `Request` to a `Response`, and can be further composed using http4s middlewares or request-transforming functions. The tapir-generated `HttpRoutes` captures from the request only what is described by the endpoint. It's completely feasible that some part of the input is read using a http4s wrapper function, which is then composed with the tapir endpoint descriptions. Moreover, "edge-case endpoints", which require some special logic not expressible using tapir, can be always implemented directly using http4s. ## Streaming The http4s interpreter accepts streaming bodies of type `Stream[F, Byte]`, as described by the `Fs2Streams` capability. Both response bodies and request bodies can be streamed. Usage: `streamBody(Fs2Streams[F])(schema, format)`. The capability can be added to the classpath independently of the interpreter through the `"com.softwaremill.sttp.shared" %% "fs2"` [dependency](https://mvnrepository.com/artifact/com.softwaremill.sttp.shared/fs2). ## Http4s backends Http4s integrates with a couple of [server backends](https://http4s.org/v1.0/integrations/), the most popular being Blaze and Ember. In the [examples](../examples.md) and throughout the docs we use Blaze, but other backends can be used as well. This means adding another dependency, such as: ```scala "org.http4s" %% "http4s-blaze-server" % Http4sVersion ``` ## Web sockets The interpreter supports web sockets, with pipes of type `Pipe[F, REQ, RESP]`. See [web sockets](../endpoint/websockets.md) for more details. However, endpoints which use web sockets need to be interpreted using the `Http4sServerInterpreter.toWebSocketRoutes` method, which returns a function `WebSocketBuilder2[F] => HttpRoutes[F]`. This can then be added to a server builder using `withHttpWebSocketApp`, for example: ```scala import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter import cats.effect.IO import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router import org.http4s.server.websocket.WebSocketBuilder2 import fs2.* import scala.concurrent.ExecutionContext given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global val wsEndpoint: PublicEndpoint[Unit, Unit, Pipe[IO, String, String], Fs2Streams[IO] with WebSockets] = endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](Fs2Streams[IO])) val wsRoutes: WebSocketBuilder2[IO] => HttpRoutes[IO] = Http4sServerInterpreter[IO]().toWebSocketRoutes(wsEndpoint.serverLogicSuccess[IO](_ => ???)) BlazeServerBuilder[IO] .withExecutionContext(summon[ExecutionContext]) .bindHttp(8080, "localhost") .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) ``` ```{note} When a close frame is received by http4s, the server seems to cancel the stream that is processing the web socket frames. This means that the `.decodeCloseRequests(true)` setting (also effective when the decoded type is optional, e.g. `Option`) is not reliable: values corresponding to close frames will not always be processed by the stream. Hence, it's recommended to avoid using this option with the http4s interpreter. ``` ## Server Sent Events The interpreter supports [SSE (Server Sent Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). For example, to define an endpoint that returns event stream: ```scala import cats.effect.IO import sttp.model.sse.ServerSentEvent import sttp.tapir.* import sttp.tapir.server.http4s.{Http4sServerInterpreter, serverSentEventsBody} val sseEndpoint = endpoint.get.out(serverSentEventsBody[IO]) val routes = Http4sServerInterpreter[IO]().toRoutes(sseEndpoint.serverLogicSuccess[IO](_ => IO(fs2.Stream(ServerSentEvent(Some("data"), None, None, None))) )) ``` ## Accessing http4s context If you'd like to access context provided by an http4s middleware, e.g. with authentication data, this can be done with a dedicated context-extracting input, `.contextIn` (or the analogous `.contextSecurityIn` for security inputs). Endpoints using such input need then to be interpreted to `org.http4s.ContextRoutes` (also known by its type alias `AuthedRoutes`) using the `.toContextRoutes` method. For example: ```scala import sttp.tapir.* import sttp.tapir.server.http4s.* import cats.effect.IO import org.http4s.ContextRoutes case class SomeCtx(actionAllowed: Boolean) // the context expected from http4s middleware def countCharacters(in: (String, SomeCtx)): IO[Either[Unit, Int]] = IO.pure( if(in._2.actionAllowed) Right[Unit, Int](in._1.length) else Left[Unit, Int](()) ) // the .contextIn extension method is imported from the sttp.tapir.server.http4s package // the Context[SomeCtx] capability requirement requires interpretation to be done using .toContextRoutes val countCharactersEndpoint: PublicEndpoint[(String, SomeCtx), Unit, Int, Context[SomeCtx]] = endpoint.in(stringBody).contextIn[SomeCtx]().out(plainBody[Int]) val countCharactersRoutes: ContextRoutes[SomeCtx, IO] = Http4sServerInterpreter[IO]() .toContextRoutes(countCharactersEndpoint.serverLogic(countCharacters _)) ``` ## Configuration The interpreter can be configured by providing an `Http4sServerOptions` value, see [server options](options.md) for details. The http4s options also includes configuration for the blocking execution context to use, and the io chunk size. # Interceptors ## Request interceptors Request interceptors intercept the whole request, and are called once for each request. They can provide additional endpoint interceptors, as well as modify the request, server endpoints, or the response. The following request interceptors are provided by default (and if enabled, called in this order): * the [metrics interceptor](observability.md), which by default is disabled * a CORS interceptor (disabled by default) * the `RejectInterceptor`, which specifies what should be done when decoding the request has failed for all interpreted endpoints. The default is to return a 405 (method not allowed), if there's at least one decode failure on the method, and a "no-match" otherwise (which is handled in an intereprter-specific manner) Request interceptors for two common scenarios can be created using the `RequestInterceptor.transformServerRequest` and `RequestInterceptor.filterServerEndpoints` methods. Note, that for most server interpreters, the server endpoints passed to the request interceptor will be pre-filtered using `FilterServerEndpoints`, as a performance optimization (these will be only the endpoints for which the request path might potentially decode successfully). To enable, disable or configure an interceptor, you'll need to modify the [server options](options.md), using the `.customiseInterceptors` method. ## Endpoint interceptors An `EndpointInterceptor` allows intercepting the handling of a request by an endpoint, when either the endpoint's inputs have been decoded successfully, or when decoding has failed. The following interceptors are used by default, and if enabled, called in this order: * exception interceptor * logging interceptor * unsupported media type interceptor * decode failure handler interceptor Note that while the request will be passed top-to-bottom, handling of the result will be done in opposite order. E.g., if the result is a failed effect (an exception), it will first be logged by the logging interceptor, and only later passed to the exception interceptor. Using `customiseInterceptors` on the options companion object, it is possible to customise the built-in interceptors. New ones can be prepended to the interceptor stack using `.prependInterceptor`, added before the decode failure interceptor using `.addInterceptor`, or appended using `.appendInterceptor`. Customisation can include removing the interceptor altogether. ## Attributes When implementing interceptors, it might be useful to take advantage of attributes, which can be attached both to requests, as well as endpoint descriptions. Attributes are keyed using an `AttributeKey`. Typically, each attribute corresponds to a unique type, and the key instance for that type can be created using `AttributeKey[T]`. The attribute values then have to be of the given type `T`. # Running as a JDK http server To expose endpoints using the [http server built into the JDK](https://docs.oracle.com/javase/8/docs/jre/api/net/httpserver/spec/com/sun/net/httpserver/package-summary.html) (`com.sun.net.httpserver`), first add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-jdkhttp-server" % "1.13.31" ``` Then, import the package: ```scala import sttp.tapir.server.jdkhttp.* ``` and use `JdkHttpServer().addEndpoints` to expose server endpoints. These methods require a single, or a list of `ServerEndpoint`s, which can be created by adding [server logic](logic.md) to an endpoint. You can use shortcut extension methods exposed by the package to transform your `Endpoint`s into `ServerEndpoint`s by calling `.handle` method family on them (an equivalent to `.serverLogic` family, but with the effect type fixed, which simplifies type inference). The `handle` naming convention was introduced to avoid conflicts with the original `serverLogic` methods and also because names are shorter. For example: ```scala import sttp.tapir.* import sttp.tapir.server.jdkhttp.* val helloWorld = endpoint .get .in("hello").in(query[String]("name")) .out(stringBody) .handle(name => Right(s"Hello, $name!")) val server: HttpServer = JdkHttpServer().addEndpoint(helloWorld).start() ``` ## Important notice: **This server runs on a single worker thread by default.** This is ok for testing, toy projects and things that never see any load. If you want it to scale please read about the `executor` configuration option below and set it accordingly. Given the `com.sun.net.httpserver` package is standardised and a part of public JDK API since JDK 18 (JEP 408) this server can be considered stable. ## Configuration The interpreter can be configured by providing an `JdkHttpServerOptions` value, see [server options](options.md) for details. Most options can be configured directly using a `JdkHttpServer` instance, such as the host and port. Possible options are: * `send404WhenRequestNotHandled`: Should a 404 response be sent, when the request hasn't been handled by defined endpoints. This is a safe default, but if there are multiple handlers for the same context path, this should be set to `false`. In that case, you can verify if the request has been handled using `JdkHttpServerInterpreter.isRequestHandled`. * `basePath`: Path under which endpoints will be mounted when mounted on JdkHttpServer instance. I.e.: basePath of `/api` and endpoint `/hello` will result with a real path of `/api/hello`. * `port`: IP port to which JdkHttpServer instance will be bound. Default is `0`, which means any random port provided by the OS. * `host`: Hostname or IP address (ie.: `localhost`, `0.0.0.0`) to which JdkHttpServer instance will be bound. Default is `0.0.0.0` which binds the server to all network interfaces available on the OS. * `executor`: Allows you to configure the `Executor` which will be used to handle HTTP requests. By default `com.sun.net.httpserver.HttpServer` uses a single thread (a calling thread executor to be precise) executor to handle traffic which might be fine for local toy projects. If you intend to use this HTTP server for any deployments that will run under load it's absolutely necessary to set an executor that will use proper thread pool to handle the load. Recommended approach is to use `JdkHttpServerOptions.httpExecutor` method to create a ThreadPoolExecutor that will scale under load. You can also use an Executor returned from any of the constructors in the `java.util.concurrent.Executors` class. Alternatively, if running with a JDK 19+ you can leverage Project Loom and use `Executors.newVirtualThreadPerTaskExecutor()` to run each request on a virtual thread. This however means it is possible for your server to be overloaded with work as each request will be given a thread with no backpressure on how many should be executed in parallel. * `httpsConfigurator`: Optional HTTPS configuration. Takes an instance of `com.sun.net.httpserver.HttpsConfigurator`, which is a thin wrapper around `javax.net.ssl.SSLContext` to configure the SSL termination for this server. * `backlogSize`: Sets the size of server's tcp connection backlog. This is the maximum number of queued incoming connections to allow on the listening socket. Queued TCP connections exceeding this limit may be rejected by the TCP implementation. If set to 0 or less the system default for backlog size will be used. Default is 0. # Server logic To interpret a single endpoint, or multiple endpoints as a server, the endpoint descriptions must be coupled with functions which implement the server logic. The shape of these functions must match the types of the inputs and outputs of the endpoint. The type of such an endpoint+logic combination is `ServerEndpoint[R, F]`, where `R` are the endpoint's requirements (websockets, streams) and `F` is the effect type of the logic, such as `Future` or `IO`. If you'd like to preserve the full type information of the inputs and outputs, you can use the `ServerEndpoint.Full[A, U, I, E, O, R, F]` type alias. For public endpoints (where the type of the security inputs is `Unit`), the server logic can be provided using the `serverLogic(f: I => F[Either[E, O]]` method. For secure endpoints, you first need to provide the security logic using `serverSecurityLogic` and then the main logic. ```{note} If you are using a synchronous server (e.g. netty-sync, nima, or jdkhttp) the `F[_]` "effect" type is set to be the identity type constructor, `type Identity[X] = X`. For such cases, the server logic can be provided using the `.handle(f: I => Either[E, O])` and `.handleSecurity` methods, which provide better type inference and readability. ``` Hence, apart from a `Endpoint[A, I, E, O, R]`, the server endpoint contains: * the server logic of type `I => F[Either[E, O]]` for public endpoint * the security logic of type `A => F[Either[E, U]]` and the main logic of type `U => I => F[Either[E, O]]` The intuition behind the `A` and `U` types is that the first is the type of authentication data, and the second of the "user" (whatever this might mean in your system), that is found provided that the authentication was successful. If either the security logic, or the main logic fails, an error of type `E` might be returned. ## Multiple input parameters Note that when dealing with endpoints which have multiple input parameters, the server logic function is a function of a *single* argument `I`, which is a tuple. This means that functions which take multiple arguments need to be converted to a function using a single argument using `.tupled`, or that you'll need to pattern-match using `case` to extract the parameters: ```scala import sttp.tapir.* import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future // using case: val echoEndpoint = endpoint .in(query[Int]("count")) .in(stringBody) .out(stringBody) .serverLogic { case (count, body) => Future.successful[Either[Unit, String]](Right(body * count)) } // using .tupled: def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? val anEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? val aServerEndpoint: ServerEndpoint[Any, Future] = anEndpoint.serverLogic((logic _).tupled) ``` ## Interpreting as a server Both a single server endpoint, and multiple endpoints can be interpreted as a server. As an example, a list of server endpoints can be converted to a Netty route: ```scala import sttp.tapir.* import sttp.tapir.server.netty.{NettyFutureServerInterpreter, FutureRoute} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global val endpoint1 = endpoint.in("hello").out(stringBody) .serverLogic { _ => Future.successful[Either[Unit, String]](Right("world")) } val endpoint2 = endpoint.in("ping").out(stringBody) .serverLogic { _ => Future.successful[Either[Unit, String]](Right("pong")) } val route: FutureRoute = NettyFutureServerInterpreter().toRoute(List(endpoint1, endpoint2)) ``` ## Recovering errors from failed effects If your `E` error type is an exception (extends `Throwable`), and if errors that occur in the server logic are represented as failed effects, you can use a variant of the methods above, which extract the error from the failed effect, and respond with the error output appropriately. This can be done with the `serverLogicRecoverErrors(f: I => F[O])` method. Note that the `E` type parameter isn't directly present here; however, the method also contains a requirement that `E` is an exception, and will only recover errors which are subtypes of `E`. Any others will be propagated without changes. For example: ```scala import sttp.tapir.* import scala.concurrent.Future case class MyError(msg: String) extends Exception val testEndpoint = endpoint .in(query[Boolean]("fail")) .errorOut(stringBody.map(MyError(_))(_.msg)) .out(stringBody) .serverLogicRecoverErrors { fail => if (fail) { Future.successful("OK") // note: no Right() wrapper } else { Future.failed(new MyError("Not OK")) // no Left() wrapper, a failed future } } ``` ## Other server logic variants There are also other variants of the methods that can be used to provide the server logic: * `serverLogicSuccess(f: I => F[O])`: specialized to the case, when the result is always a success (no errors are possible) * `serverLogicError(f: I => F[E])`: similarly for endpoints which always return an error * `serverLogicPure(f: I => Either[E, O])`: if the server logic function is pure, that is returns a strict value, not a description of side-effects * `serverLogicOption(f: I => F[Option[O]])`: if the error type is a `Unit`, a `None` results is treated as an error * `serverLogicRightErrorOrSuccess(f: I => F[Either[RE, O]])`: if the error type is an `Either`, e.g. when using `errorOutEither`, this method accepts server logic that returns either success or the `Right` error type. Use of this method avoids having to wrap the returned error in `Right`. * `serverLogicLeftErrorOrSuccess(f: I => F[Either[LE, O]])`: similarly, this accepts server logic which returns the `Left` error type or success Similar variants are available to provide the security logic. ## Re-usable security logic Quite often the security logic is shared among multiple endpoints. For secure endpoints, which have [security inputs](../endpoint/security.md) defined, the security logic needs to be provided first, followed by the main logic. This can be done either on a complete endpoint, where all of the inputs/outputs are provided, as a two-step process. Alternatively, a base "secure" endpoint can be defined, with the security inputs and security logic provided. As this is an immutable value, such an endpoint can be then extended multiple times, by adding more regular inputs and outputs, each time yielding a new immutable representation. For each such extension, the main server logic still needs to be provided. For example, we can create a partial server endpoint given the security logic, and an endpoint with security inputs: ```scala import sttp.tapir.* import sttp.tapir.server.* import scala.concurrent.{ExecutionContext, Future} implicit val ec: ExecutionContext = ExecutionContext.global case class User(name: String) def authLogic(token: String): Future[Either[Int, User]] = Future { if (token == "secret") Right(User("Spock")) else Left(1001) // error code } val secureEndpoint: PartialServerEndpoint[String, User, Unit, Int, Unit, Any, Future] = endpoint .securityIn(header[String]("X-AUTH-TOKEN")) .errorOut(plainBody[Int]) .serverSecurityLogic(authLogic) ``` The result is a value of type `PartialServerEndpoint`, which can be extended with further inputs and outputs, just as a normal endpoint. An exception are error outputs, for which new output variants can be provided using the family of `.errorOutVariant` methods, but they cannot be arbitrarily extended; this is similar to defining the entire error output as a [`oneOf`](../endpoint/oneof.md). Then, we can complete the endpoint to a `ServerEndpoint` by providing the main server logic using `.serverLogic` or any of the other variants: ```scala val secureHelloWorld1WithLogic: ServerEndpoint[Any, Future] = secureEndpoint.get .in("hello1") .in(query[String]("salutation")) .out(stringBody) .serverLogicSuccess { (user: User) => (salutation: String) => Future.successful(s"${salutation}, ${user.name}!") } ``` ### Security logic with outputs When using `.serverSecurityLogic`, the result of the security function is treated as an input to the main server logic. However, it might be desirable to provide some output as part of the security logic. This is possible using `.serverSecurityLogicWithOutput` and its variants. The provided security function has to return a value for the output defined so far, and a value that will be provided to the main server logic. The security output will contribute directly to the output of the whole endpoint, unless an error response is returned. Additional outputs can be then added to the resulting partial endpoint. ## Status codes By default, successful responses are returned with the `200 OK` status code, and errors with `400 Bad Request`. However, this can be customised by using a [status code output](../endpoint/ios.md). ## Additional security logic In some cases, e.g. when using some pre-defined public server endpoints, such as ones for [serving static content](../endpoint/static.md) or to expose the [Swagger UI](../docs/openapi.md), it might be necessary to add a security check. One way to achieve this is extending the pre-defined endpoint description with security inputs, and then re-using the appropriate server logic, with custom security logic, but this requires non-trivial amount of code. For such situations, a `ServerLogic.prependSecurity` method is provided. It accepts a security input description, along with an error output (for security errors) and the security logic to add. This additional security logic is run before the security logic defined in the endpoint so far (if any). For example: ```scala import sttp.tapir.* import sttp.tapir.files.* import scala.concurrent.Future import sttp.model.StatusCode val secureFileEndpoints = staticFilesServerEndpoints[Future]("secure")("/home/data") .map(_.prependSecurity(auth.bearer[String](), statusCode(StatusCode.Forbidden)) { token => Future.successful(if (token.startsWith("secret")) Right(()) else Left(())) }) ``` ## File handling When: * receiving a multipart request in a Tapir server, and mapping some parts to files * receiving the request body as a file all created files are treated as temporary, and will be deleted when the request processing completes (regardless of the outcome - HTTP success, HTTP failure or exception). # Running as a Netty-based server To expose an endpoint using a [Netty](https://netty.io)-based server, first add the following dependency: ```scala // if you want to use Java 21+ Virtual Threads & direct-style: "com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.13.31" // if you are using Future: "com.softwaremill.sttp.tapir" %% "tapir-netty-server" % "1.13.31" // if you are using cats-effect: "com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.13.31" // if you are using zio: "com.softwaremill.sttp.tapir" %% "tapir-netty-server-zio" % "1.13.31" ``` Then, use: - `NettySyncServer().addEndpoints` to expose direct-style server endpoints (using Virtual Threads). Streaming & WebSockets are supported with Ox Flows. - `NettyFutureServer().addEndpoints` to expose `Future`-based server endpoints. - `NettyCatsServer().addEndpoints` to expose `F`-based server endpoints, where `F` is any cats-effect supported effect. Streaming & WebSockets are supported with fs2. - `NettyZioServer().addEndpoints` to expose `ZIO`-based server endpoints, where `R` represents ZIO requirements supported effect. Streaming & WebSockets are supported with ZIO Streams. These methods require a single, or a list of `ServerEndpoint`s, which can be created by adding [server logic](logic.md) to an endpoint. For example, using direct style: ```scala import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer val helloWorld = endpoint .get .in("hello").in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") NettySyncServer().addEndpoint(helloWorld).startAndWait() ``` ## Direct-style The `tapir-netty-server-sync` provides a direct-style server using Netty behind the scenes. The implementation uses `Identity[T]` as the "effect" type. `Identity[A]` simplifies to just `A`, representing direct style. The module is available only for Scala 3. See [examples](../examples.md) labeled with `Direct`. To provide server logic for an endpoint when using the `-sync` server, you can use the dedicated `handle...` methods, and its variants. This provides better type inference. To learn more about handling concurrency and streaming with Ox and `Flow`s, see its [documentation](https://ox.softwaremill.com/). ## Configuration The interpreters can be configured by providing an `Netty[Sync|Future|...]ServerOptions` value, see [server options](options.md) for details. Some options can be configured directly using a `Netty[Sync|Future|...]Server` instance, such as the host and port. Others can be passed using the `Netty[Sync|Future|...]Server (options)` methods. Options may also be overridden when adding endpoints. For example: ```scala import sttp.tapir.server.netty.NettyConfig import sttp.tapir.server.netty.sync.{NettySyncServer, NettySyncServerOptions} // customising the port NettySyncServer().port(9090).addEndpoints(???) // customising the interceptors NettySyncServer(NettySyncServerOptions.customiseInterceptors.serverLog(None).options) // customise Netty config NettySyncServer(NettyConfig.default.socketBacklog(256)) ``` ```{note} Unlike other server interpreters, the Netty-based servers are by default configured to return a 404, in case none of the given endpoints match a request. This can be changed by using a different `RejectHandler`. This is due to the fact that usually no other routes (other than generated from Tapir's endpoints) are added to a Netty server. ``` ### Server socket configuration `NettyConfig` exposes a number of configuration options which allows to customise the server socket, such as: * request timeout * connection timeout * linger timeout * graceful shutdown timeout: when stopped e.g. using `NettySyncServerBinding.stop()`, it's ensured that the server will wait at most 10 seconds for in-flight requests to complete, while rejecting all new requests with 503 during this period; afterwards, all server resources are closed * server header * maximum number of connections * custom netty pipeline & low-level logging handlers For example, to change the request timeout: ```scala import sttp.tapir.server.netty.NettyConfig import scala.concurrent.duration.* val config = NettyConfig.default.requestTimeout(5.seconds) ``` ## Web sockets ### tapir-netty-server-sync In the Loom-based backend, Tapir uses [Ox](https://ox.softwaremill.com) to manage concurrency, and your transformation pipeline should be represented as `Flow[A] => Flow[B]`. Any forks started within this function will be run under a safely isolated internal scope. See [examples/websocket/WebSocketNettySyncServer.scala](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala) for a full example. ```{note} The pipeline transforms a source of incoming web socket messages (received from the client), into a source of outgoing web socket messages (which will be sent to the client), within some concurrency scope. Once the incoming source is done, the client has closed the connection. In that case, remember to close the outgoing source as well: otherwise the scope will leak and won't be closed. An error will be logged if the outgoing channel is not closed within a timeout after a close frame is received. ``` ### tapir-netty-server-cats The Cats Effects interpreter supports web sockets, with pipes of type `fs2.Pipe[F, REQ, RESP]`. See [web sockets](../endpoint/websockets.md) for more details. To create a web socket endpoint, use Tapir's `out(webSocketBody)` output type: ```scala import cats.effect.kernel.Resource import cats.effect.{IO, ResourceApp} import cats.syntax.all.* import fs2.Pipe import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.* import sttp.tapir.server.netty.cats.NettyCatsServer import sttp.ws.WebSocketFrame import scala.concurrent.duration.* object WebSocketsNettyCatsServer extends ResourceApp.Forever { // Web socket endpoint val wsEndpoint = endpoint.get .in("ws") .out( webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](Fs2Streams[IO]) .concatenateFragmentedFrames(false) // All these options are supported by tapir-netty .ignorePong(true) .autoPongOnPing(true) .decodeCloseRequests(false) .decodeCloseResponses(false) .autoPing(Some((10.seconds, WebSocketFrame.Ping("ping-content".getBytes)))) ) // Your processor transforming a stream of requests into a stream of responses val pipe: Pipe[IO, String, String] = requestStream => requestStream.evalMap(str => IO.pure(str.toUpperCase)) // Alternatively, requests can be ignored and the backend can be turned into a stream emitting frames to the client: // val pipe: Pipe[IO, String, String] = requestStream => someDataEmittingStream.concurrently(requestStream.as(())) val wsServerEndpoint = wsEndpoint.serverLogicSuccess(_ => IO.pure(pipe)) // A regular /GET endpoint val helloWorldEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) val helloWorldServerEndpoint = helloWorldEndpoint .serverLogicSuccess(name => IO.pure(s"Hello, $name!")) override def run(args: List[String]) = NettyCatsServer .io() .flatMap { server => Resource .make( server .port(8080) .host("localhost") .addEndpoints(List(wsServerEndpoint, helloWorldServerEndpoint)) .start() )(_.stop()) .as(()) } } ``` ## Response compression The Netty server supports automatic HTTP response compression using gzip or deflate encoding. When enabled, the server will: - Inspect the client's `Accept-Encoding` header - Compress responses when the client supports gzip or deflate - Add the appropriate `Content-Encoding` header to compressed responses Compression is disabled by default. To enable it: ```scala import sttp.tapir.server.netty.{NettyConfig, NettyCompressionConfig, NettyFutureServer} import scala.concurrent.ExecutionContext.Implicits.global // Enable compression with Netty's default settings val config1 = NettyConfig.default.withCompressionEnabled // Or use the compression config explicitly val config2 = NettyConfig.default.compressionConfig(NettyCompressionConfig.enabled) // Start server with compression enabled NettyFutureServer(config1).addEndpoints(???) ``` ### When to use compression Compression is most beneficial for: - Large text responses (JSON, XML, HTML, etc.) - Responses over slow networks - APIs with high bandwidth usage The compression is applied automatically by Netty based on the client's `Accept-Encoding` header. All responses that match the client's accepted encodings will be compressed using Netty's default compression settings. ## Server Sent Events ### tapir-netty-server-sync The interpreter supports [SSE (Server Sent Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). For example, to define an endpoint that returns event stream: ```scala import ox.flow.Flow import sttp.model.sse.ServerSentEvent import sttp.tapir.* import sttp.tapir.server.netty.sync.serverSentEventsBody import scala.concurrent.duration.* val sseEndpoint = endpoint.get.out(serverSentEventsBody) val sseFlow = Flow .tick(1.second) // emit a new event every second .take(10) .map(_ => s"Event at ${System.currentTimeMillis()}") .map(event => ServerSentEvent(data = Some(event))) val sseServerEndpoint = sseEndpoint.handleSuccess(_ => sseFlow) ``` ## Domain socket support There is possibility to use Domain socket instead of TCP for handling traffic. ```scala import sttp.tapir.* import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureDomainSocketBinding} import java.nio.file.Paths import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import io.netty.channel.unix.DomainSocketAddress val serverBinding: Future[NettyFutureDomainSocketBinding] = NettyFutureServer().addEndpoint( endpoint.get.in("hello").in(query[String]("name")).out(stringBody).serverLogic(name => Future.successful[Either[Unit, String]](Right(s"Hello, $name!"))) ) .startUsingDomainSocket(Paths.get(System.getProperty("java.io.tmpdir"), "hello")) ``` ## Logging By default, [logging](debugging.md) of handled requests and exceptions is enabled, and uses an slf4j logger. # Running as a Helidon Níma server ```{note} Helidon Níma requires JDK supporting Project Loom threading (JDK21 or newer). ``` To expose an endpoint as a [Helidon Níma](https://helidon.io/nima) server, first add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-nima-server" % "1.13.31" ``` Loom-managed concurrency uses direct style instead of effect wrappers like `Future[T]` or `IO[T]`. Because of this, Tapir endpoints defined for Nima server use `Identity[T]`, which provides compatibility, while effectively means just `T`. Such endpoints are then processed through `NimaServerInterpreter` in order to obtain an `io.helidon.webserver.http.Handler`: ```scala import io.helidon.webserver.WebServer import sttp.tapir.* import sttp.shared.Identity import sttp.tapir.server.nima.NimaServerInterpreter val helloEndpoint = endpoint.get .in("hello") .out(stringBody) .handleSuccess { _ => Thread.sleep(1000) "hello, world!" } val handler = NimaServerInterpreter().toHandler(List(helloEndpoint)) WebServer .builder() .routing { builder => builder.any(handler) () } .port(8080) .build() .start() ``` # Observability Observability includes metrics & tracing integrations. ## Metrics Metrics collection is possible by creating `Metric` instances and adding them to server options via `MetricsInterceptor`. Certain endpoints can be ignored by adding their definitions to `ignoreEndpoints` list. `Metric` wraps an aggregation object (like a counter or gauge), and needs to implement the `onRequest` function, which returns an `EndpointMetric` instance. `Metric.onRequest` is used to create the proper metric description. Apart from triggering an initial metric, additional data might be gathered there, like getting current timestamp and passing it down to `EndpointMetric` callbacks which are then executed in certain points of request processing. There are six callbacks in `EndpointMetric`: 1. `onEndpointRequest` - called after a request matches an endpoint (inputs are successfully decoded), or when decoding inputs fails, but a downstream interceptor provides a response. 2. `onResponseHeaders` - called after response headers are assembled. 3. `onResponseBody` - called after response body is complete. Note that the response body might be lazily produced. 4. `onException` - called after exception is thrown (in underlying streamed body, and/or on any other exception when there's no default response). 5. `onInterceptorResponse` - called when the response was generated by a request handler in an interceptor (e.g., a 404 response from the reject interceptor), meaning no other metric callbacks associated with the response have been invoked. No endpoint or decode failures are associated with the response in such cases. 6. `onDecodeFailure` - called when all endpoints failed to decode the request. After `Metric.onRequest` is called, it's guaranteed that exactly one callback sequence on `EndpointMetric` will be invoked: * `onEndpointRequest` followed by `onResponseHeaders` and `onResponseBody` ("happy path") * `onEndpointRequest` followed by `onException` * `onException` (exception in interceptor) * `onInterceptorResponse` * `onDecodeFailure` ## Metric labels By default, request metrics are labeled by method and if available, the matching endpoint's path template. Response labels are additionally labelled by status code group. For example GET endpoint like `http://h:p/api/persons?name=Mike` returning 200 response will be labeled as `path="api/persons", method="GET", status="2xx"`. Query params are omitted by default, but it's possible to include them as shown in example below. If the path contains captures, the label will include the path capture name instead of the actual value, e.g. `api/persons/{name}`. Labels for default metrics can be customized. Labels are split into three categories based on the data they need: - `forRequest`: labels that only need the `ServerRequest` (e.g., method, protocol, headers) - `forEndpoint`: labels that only need the `AnyEndpoint` (e.g., path template, endpoint metadata) - `forResponse`: labels that need the response or exception (e.g., status code, error type) For example: ```scala import sttp.tapir.server.metrics.MetricLabels val labels = MetricLabels( forRequest = List( "method" -> { req => req.method.method }, "protocol" -> { req => req.protocol } ), forEndpoint = List( "path" -> { ep => ep.showPathTemplate() } ), forResponse = Nil ) ``` ## Prometheus metrics Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % "1.13.31" ``` `PrometheusMetrics` encapsulates `PrometheusReqistry` and `Metric` instances. It provides several ready to use metrics as well as an endpoint definition to read the metrics & expose them to the Prometheus server. For example, using `NettyFutureServerInterpreter`: ```scala import sttp.tapir.server.metrics.prometheus.PrometheusMetrics import sttp.tapir.server.netty.{NettyFutureServerInterpreter, NettyFutureServerOptions, FutureRoute} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global // an instance with default metrics; use PrometheusMetrics[Future]() for an empty one val prometheusMetrics = PrometheusMetrics.default[Future]() // enable metrics collection val serverOptions: NettyFutureServerOptions = NettyFutureServerOptions .customiseInterceptors .metricsInterceptor(prometheusMetrics.metricsInterceptor()) .options // route which exposes the current metrics values val routes: FutureRoute = NettyFutureServerInterpreter(serverOptions).toRoute(prometheusMetrics.metricsEndpoint) ``` By default, the following metrics are exposed: * `tapir_request_active{method}` (gauge) * `tapir_request_total{path, method, status}` (counter) * `tapir_request_duration_seconds{path, method, status, phase}` (histogram) The namespace and label names/values can be customised when creating the `PrometheusMetrics` instance. ### Custom metrics To create and add custom metrics: ```scala import sttp.tapir.server.metrics.prometheus.PrometheusMetrics import sttp.tapir.server.metrics.{EndpointMetric, Metric} import io.prometheus.metrics.core.metrics.{Counter, Gauge, Histogram} import io.prometheus.metrics.model.registry.PrometheusRegistry import scala.concurrent.Future // Metric for counting responses labeled by path, method and status code val responsesTotal = Metric[Future, Counter]( Counter .builder() .name("tapir_responses_total") .help("HTTP responses") .labelNames("path", "method", "status") .register(PrometheusRegistry.defaultRegistry), onRequest = { (req, counter, _) => Future.successful( EndpointMetric() .onResponseBody { (ep, res) => Future.successful { val path = ep.showPathTemplate() val method = req.method.method val status = res.code.toString() counter.labelValues(path, method, status).inc() } } ) } ) val prometheusMetrics = PrometheusMetrics[Future]("tapir", PrometheusRegistry.defaultRegistry) .addCustom(responsesTotal) ``` ## Prometheus simpleclient metrics ```{warning} Prometheus simpleclient is deprecated and will be removed in a future version. It's recommended to use `tapir-prometheus-metrics` instead, and only use this module as a temporary migration path. ``` Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-prometheus-simpleclient-metrics" % "1.13.31" ``` `PrometheusMetrics` encapsulates `CollectorReqistry` and `Metric` instances. It provides several ready to use metrics as well as an endpoint definition to read the metrics & expose them to the Prometheus server. For example, using `NettyFutureServerInterpreter`: ```scala import sttp.tapir.server.metrics.prometheus_simpleclient.PrometheusMetrics import sttp.tapir.server.netty.{NettyFutureServerInterpreter, NettyFutureServerOptions, FutureRoute} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global // an instance with default metrics; use PrometheusMetrics[Future]() for an empty one val prometheusMetrics = PrometheusMetrics.default[Future]() // enable metrics collection val serverOptions: NettyFutureServerOptions = NettyFutureServerOptions .customiseInterceptors .metricsInterceptor(prometheusMetrics.metricsInterceptor()) .options // route which exposes the current metrics values val routes: FutureRoute = NettyFutureServerInterpreter(serverOptions).toRoute(prometheusMetrics.metricsEndpoint) ``` By default, the following metrics are exposed: * `tapir_request_active{path, method}` (gauge) * `tapir_request_total{path, method, status}` (counter) * `tapir_request_duration_seconds{path, method, status, phase}` (histogram) The namespace and label names/values can be customised when creating the `PrometheusMetrics` instance. ### Custom metrics To create and add custom metrics: ```scala import sttp.tapir.server.metrics.prometheus_simpleclient.PrometheusMetrics import sttp.tapir.server.metrics.{EndpointMetric, Metric} import io.prometheus.client.{CollectorRegistry, Counter} import scala.concurrent.Future // Metric for counting responses labeled by path, method and status code val responsesTotal = Metric[Future, Counter]( Counter .build() .namespace("tapir") .name("responses_total") .help("HTTP responses") .labelNames("path", "method", "status") .register(CollectorRegistry.defaultRegistry), onRequest = { (req, counter, _) => Future.successful( EndpointMetric() .onResponseBody { (ep, res) => Future.successful { val path = ep.showPathTemplate() val method = req.method.method val status = res.code.toString() counter.labels(path, method, status).inc() } } ) } ) val prometheusMetrics = PrometheusMetrics[Future]("tapir", CollectorRegistry.defaultRegistry) .addCustom(responsesTotal) ``` ## OpenTelemetry metrics Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-metrics" % "1.13.31" ``` OpenTelemetry metrics are vendor-agnostic and can be exported using one of [exporters](https://github.com/open-telemetry/opentelemetry-java/tree/main/exporters) from SDK. `OpenTelemetryMetrics` encapsulates metric instances and needs a `Meter` from OpenTelemetry API to create default metrics, simply: ```scala import sttp.tapir.server.metrics.opentelemetry.OpenTelemetryMetrics import io.opentelemetry.api.metrics.{Meter, MeterProvider} import scala.concurrent.Future val provider: MeterProvider = ??? val meter: Meter = provider.get("instrumentation-name") val metrics = OpenTelemetryMetrics.default[Future](meter) val metricsInterceptor = metrics.metricsInterceptor() // add to your server options ``` ## otel4s OpenTelemetry metrics Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-otel4s-metrics" % "1.13.31" ``` The `Otel4sMetrics` provides integration with the [otel4s](https://typelevel.org/otel4s/) library for OpenTelemetry metrics. This allows you to create metrics for your tapir endpoints using a purely functional API. `Otel4sMetrics` encapsulates metric instances and needs a `Meter[F]` from `otel4s` to create default metrics. It should be set as `metricsInterceptor` of your ServerOptions: Example using Http4s: ```scala import cats.effect.IO import org.typelevel.otel4s.oteljava.OtelJava import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.server.http4s.Http4sServerOptions import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.metrics.otel4s.Otel4sMetrics OtelJava .autoConfigured[IO]() .use { otel4s => otel4s.meterProvider.get("meter-name").flatMap { meter => val endpoints: List[ServerEndpoint[Any, IO]] = ??? val routes = Http4sServerInterpreter[IO]( Http4sServerOptions .customiseInterceptors[IO] .metricsInterceptor(Otel4sMetrics.default(meter).metricsInterceptor()) .options ).toRoutes(endpoints) // start your server ??? } } ``` By default, the following metrics are exposed, following the [otel semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/http-metrics): * `http.server.active_requests` (up-down-counter) * `http.server.requests.total` (counter) * `http.server.request.duration` (histogram) ## Datadog Metrics Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-datadog-metrics" % "1.13.31" ``` Datadog metrics are sent as Datadog custom metrics through [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/) protocol. `DatadogMetrics` uses `StatsDClient` to send the metrics, and its settings such as host, port, etc. depend on it. For example: ```scala import com.timgroup.statsd.{NonBlockingStatsDClientBuilder, StatsDClient} import sttp.tapir.server.metrics.datadog.DatadogMetrics import scala.concurrent.Future val statsdClient: StatsDClient = new NonBlockingStatsDClientBuilder() .hostname("localhost") // Datadog Agent's hostname .port(8125) // Datadog Agent's port (UDP) .build() val metrics = DatadogMetrics.default[Future](statsdClient) ``` ### Custom Metrics To create and add custom metrics: ```scala import com.timgroup.statsd.{NonBlockingStatsDClientBuilder, StatsDClient} import sttp.tapir.server.metrics.datadog.DatadogMetrics import sttp.tapir.server.metrics.datadog.DatadogMetrics.Counter import sttp.tapir.server.metrics.{EndpointMetric, Metric} import scala.concurrent.Future val statsdClient: StatsDClient = new NonBlockingStatsDClientBuilder() .hostname("localhost") .port(8125) .build() // Metric for counting responses labeled by path, method and status code val responsesTotal = Metric[Future, Counter]( Counter("tapir.responses_total.count")(statsdClient), onRequest = (req, counter, _) => Future.successful( EndpointMetric() .onResponseBody { (ep, res) => Future.successful { val labels = List( s"path:${ep.showPathTemplate()}", s"method:${req.method.method}", s"status:${res.code.toString()}" ) counter.increment(labels) } } ) ) val datadogMetrics = DatadogMetrics.default[Future](statsdClient) .addCustom(responsesTotal) ``` ## Zio Metrics Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-zio-metrics" % "1.13.31" ``` Metrics have been integrated into ZIO core in ZIO2. [Monitoring a ZIO Application Using ZIO's Built-in Metric System](https://zio.dev/guides/tutorials/monitor-a-zio-application-using-zio-built-in-metric-system/). ### Collecting Metrics ```scala import sttp.tapir.server.metrics.zio.ZioMetrics import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor import zio.{Task, ZIO} val metrics: ZioMetrics[Task] = ZioMetrics.default[Task]() val metricsInterceptor: MetricsRequestInterceptor[Task] = metrics.metricsInterceptor() ``` ### Example Publishing Metrics Endpoint Zio metrics publishing functionality is provided by the zio ecosystem library [zio-metrics-connectors](https://github.com/zio/zio-metrics-connectors). [Dependencies/Examples](https://zio.dev/guides/tutorials/monitor-a-zio-application-using-zio-built-in-metric-system/#adding-dependencies-to-the-project) ```scala libraryDependencies += "dev.zio" %% "zio-metrics-connectors" % "2.0.0-RC6" ``` Example zio metrics prometheus publisher style tapir metrics endpoint. ```scala import sttp.tapir.{endpoint, stringBody} import zio.* import zio.metrics.connectors.MetricsConfig import zio.metrics.connectors.prometheus.{PrometheusPublisher, prometheusLayer, publisherLayer} import zio.metrics.jvm.DefaultJvmMetrics object ZioEndpoint: /** DefaultJvmMetrics.live.orDie >+> is optional if you want JVM metrics */ private val layer = DefaultJvmMetrics.live.orDie >+> ZLayer.make[PrometheusPublisher]( ZLayer.succeed(MetricsConfig(1.seconds)), prometheusLayer, publisherLayer ) private val unsafeLayers = Unsafe.unsafe { implicit u => Runtime.unsafe.fromLayer(layer) } def getMetricsEffect: ZIO[Any, Nothing, String] = Unsafe.unsafe { implicit u => unsafeLayers.run(ZIO .serviceWithZIO[PrometheusPublisher](_.get) ) } val metricsEndpoint = endpoint.get.in("metrics").out(stringBody).serverLogicSuccess(_ => getMetricsEffect) ``` ## OpenTelemetry tracing Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing" % "1.13.31" ``` OpenTelemetry tracing is vendor-agnostic and can be exported using an exporters, such as Jaeger, Zipkin, DataDog, Grafana, etc. Currently, a `OpenTelemetryTracing` interceptor is available, which creates a span for each request, populating the context appropriately (with context extracted from the request headers, the request method, path, status code, etc.). Any spans created as part of the server's logic are then correlated with the request-span, into a single trace. To propagate the context, the configured OpenTelemetry `ContextStorage` is used, which by default is `ThreadLocal`-based, which works with synchronous/direct-style environments, including ones leveraging Ox and virtual threads. [[Future]]s are supported through instrumentation provided by the [OpenTelemetry javaagent](https://opentelemetry.io/docs/zero-code/java/agent/). For functional effect systems, usually a dedicated integration library is required. The interceptor should be added before any others, so that it handles the request early. E.g.: ```scala import io.opentelemetry.api.OpenTelemetry import sttp.tapir.server.netty.sync.{NettySyncServer, NettySyncServerOptions} import sttp.tapir.server.tracing.opentelemetry.OpenTelemetryTracing val otel: OpenTelemetry = ??? val serverOptions: NettySyncServerOptions = NettySyncServerOptions.customiseInterceptors .prependInterceptor(OpenTelemetryTracing(otel)) .options NettySyncServer().options(serverOptions).addEndpoint(???).startAndWait() ``` ## otel4s OpenTelemetry tracing Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-otel4s-tracing" % "1.13.31" ``` The `Otel4sTracing` interceptor provides integration with the [otel4s](https://typelevel.org/otel4s/) library for OpenTelemetry tracing. This allows you to create traces for your tapir endpoints using a purely functional API. `Otel4sTracing` creates a span for each request, extracts context from request headers, and populates spans with relevant metadata (request method, path, status code). All spans created as part of the request processing will be properly correlated into a single trace. For details on context propagation with otel4s, see the [official documentation](https://typelevel.org/otel4s/oteljava/tracing-context-propagation.html). The interceptor should be added before any others to ensure it handles the request early: Example using Http4s: ```scala import cats.effect.IO import org.typelevel.otel4s.oteljava.OtelJava import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.server.http4s.Http4sServerOptions import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.tracing.otel4s.Otel4sTracing OtelJava .autoConfigured[IO]() .use { otel4s => otel4s.tracerProvider.get("tracer-name").flatMap { tracer => val endpoints: List[ServerEndpoint[Any, IO]] = ??? val routes = Http4sServerInterpreter[IO](Http4sServerOptions.default[IO].prependInterceptor(Otel4sTracing(tracer))) .toRoutes(endpoints) // start your server ??? } } ``` ## Tracing when no endpoints match a request When no endpoints match a request, the interceptor will still create a span for the request. However, if no response is returned, no response-related attributes will be added to the span. This is because other routes in the host server might still serve the request. If a default response (e.g. a `404 Not Found`) should be produced, this should be enabled using the [reject interceptor](errors.md). Such a setup assumes that there are no other routes in the server, after the Tapir server interpreter is invoked. ## ZIO OpenTelemetry ZIO OpenTelemetry tracing is provided by the `tapir-zio-opentelemetry` module. It is built on top of the [ZIO OpenTelemetry](https://zio.dev/zio-telemetry/) (zio-telemetry) library, and creates a span for each request handled by a tapir endpoint. Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-zio-opentelemetry" % "1.13.31" ``` The module provides the `ZIOpenTelemetryTracing` request interceptor. Prepend it to the interpreter's interceptors, so that it runs as early as possible, passing a `zio.telemetry.opentelemetry.tracing.Tracing` instance: ```scala import sttp.tapir.server.ziohttp.ZioHttpServerOptions import sttp.tapir.server.ziopentelemetry.ZIOpenTelemetryTracing import zio.telemetry.opentelemetry.tracing.Tracing def serverOptions(tracing: Tracing): ZioHttpServerOptions[Any] = ZioHttpServerOptions.customiseInterceptors .prependInterceptor(ZIOpenTelemetryTracing(tracing)) .options ``` Span names and attributes can be customised through `ZIOpenTelemetryTracingConfig`. The `Tracing` instance is created from an OpenTelemetry SDK using the [zio-telemetry](https://zio.dev/zio-telemetry/) library; see its documentation for setting up the SDK, exporters and providers. # Server options Each interpreter can be configured using an options object, which includes: * how to create a file (when receiving a response that is mapped to a file, or when reading a file-mapped multipart part) * if, and how to handle exceptions (see [error handling](errors.md)) * if, and how to log requests (see [logging & debugging](debugging.md)) * how to handle decode failures (see [error handling](errors.md)) * additional user-provided [interceptors](interceptors.md) To use custom server options pass them as an argument to the interpreter's `apply` method. For example, for `PekkoHttpServerOptions` and `PekkoHttpServerInterpreter`: ```scala import sttp.tapir.server.interceptor.decodefailure.DecodeFailureHandler import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future val customDecodeFailureHandler: DecodeFailureHandler[Future] = ??? val customServerOptions: PekkoHttpServerOptions = PekkoHttpServerOptions .customiseInterceptors .decodeFailureHandler(customDecodeFailureHandler) .options PekkoHttpServerInterpreter(customServerOptions) ``` ## Hiding authenticated endpoints By default, if authentication credentials are missing for an endpoint which defines [authentication inputs](../endpoint/security.md), a `401 Unauthorized` response is returned. If you would instead prefer to hide the fact that such an endpoint exists from the client, a `404 Not Found` can be returned instead by using a different decode failure handler. For example, using akka-http: ```scala import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future val customServerOptions: PekkoHttpServerOptions = PekkoHttpServerOptions .customiseInterceptors .decodeFailureHandler(DefaultDecodeFailureHandler.hideEndpointsWithAuth[Future]) .options PekkoHttpServerInterpreter(customServerOptions) ``` Note however, that it can still be possible to discover the existence of certain endpoints using timing attacks. Moreover, any `400 Bad Request` response are also converted to a `404`, making working the endpoint harder - there's no feedback as to what kind of query parameters or headers might be missing or malformed. This applies only to inputs, which fail do decode. For scenarios where the inputs decode successfully, but the authentication should fail, an error result should be returned from the [security logic](logic.md). # Path matching When a server receives a request, it must determine which endpoint might potentially handle it. In order to do so, the endpoints are first pre-filtered, so that only endpoints where the path shape matches (that is, the number of path inputs/segments must match, as well as any constant segments) are considered. Next, the inputs are decoded, starting from the method. If the method inputs decode successfully, the path inputs are decoded. ## Exact matches and trailing slashes The path must match *exactly* - any remaining path segments will cause the endpoint not to match the request. However, extra trailing slashes are allowed. For example, `endpoint.in("api")` will match `/api`, `/api/`, but won't match `/`, `/api/users`. To match only the root path, use an empty string: `endpoint.in("")` will match `http://server.com/` and `http://server.com`. ## Matching any path As with all other types of inputs, if no path input/path segments are defined, any path will match. To match a path prefix, first define inputs which match the path prefix, and then capture any remaining part using `paths`, e.g.: `endpoint.in("api" / "download").in(paths)`. ## Decoding failures If decoding a path input fails, a `400 Bad Request` will be returned to the user. When using the default decode failure handler, this can be customised to instead attempt decoding the next endpoint, by adding an attribute to the path input with `.onDecodeFailureNextEndpoint`. Alternatively, another strategy can be implemented by using a completely custom decode failure handler. Both topics are covered in more detail in the documentation of [error handling](errors.md). ## Endpoint ordering The order in which endpoints are given to the server interpreter matters. If the shape of multiple endpoints matches certain requests, such endpoints should be listed from the most specific, to the least specific. For example, an endpoint `endpoint.in("users" / "find")` is more specific than `endpoint.in("users" / path[Int]("id"))`, and should be listed first: otherwise attempting to decode `"find"` as an integer will cause an error. More complex scenarios of path matching can be implemented using the approach described in the previous section. ## Security and regular inputs Any security inputs are decoded first, before regular inputs. Hence, it is not uncommon for the security inputs to define the path prefix of the endpoint, along with any components that the security input should capture. As noted in the section on [security inputs](../endpoint/security.md), it is completely fine for the security inputs to contain any kind of inputs, including path segment and path captures. The security/regular distinction only influences the decoding order, and which parameters are available to which part of the server logic. # Running as a pekko-http server To expose an endpoint as a [pekko-http](https://pekko.apache.org/docs/pekko-http/current/) server, first add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.13.31" ``` This will transitively pull some Pekko modules. If you want to force your own Pekko version, use sbt exclusion. Mind the Scala version in artifact name: ```scala "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.13.31" exclude("org.apache.pekko", "pekko-stream_2.12") ``` Now import the object: ```scala import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter ``` ## Using `toRoute` The `toRoute` method requires a single, or a list of `ServerEndpoint`s, which can be created by adding [server logic](logic.md) to an endpoint. For example: ```scala import sttp.tapir.* import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.Future import org.apache.pekko.http.scaladsl.server.Route import scala.concurrent.ExecutionContext.Implicits.global def countCharacters(s: String): Future[Either[Unit, Int]] = Future.successful(Right[Unit, Int](s.length)) val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) val countCharactersRoute: Route = PekkoHttpServerInterpreter().toRoute(countCharactersEndpoint.serverLogic(countCharacters)) ``` ## Combining directives The tapir-generated `Route` captures from the request only what is described by the endpoint. Combine with other pekko-http directives to add additional behavior, or get more information from the request. For example, wrap the tapir-generated route in a metrics route, or nest a security directive in the tapir-generated directive. Edge-case endpoints, which require special logic not expressible using tapir, can be implemented directly using pekko-http. For example: ```scala import sttp.tapir.* import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import org.apache.pekko.http.scaladsl.server.* import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global class Special def metricsDirective: Directive0 = ??? def specialDirective: Directive1[Special] = ??? val tapirEndpoint: PublicEndpoint[String, Unit, Unit, Any] = endpoint.in(path[String]("input")) val myRoute: Route = metricsDirective { specialDirective { special => PekkoHttpServerInterpreter().toRoute(tapirEndpoint.serverLogic[Future] { input => ??? /* here we can use both `special` and `input` values */ }) } } ``` ## Streaming The pekko-http interpreter accepts streaming bodies of type `Source[ByteString, Any]`, as described by the `PekkoStreams` capability. Both response bodies and request bodies can be streamed. Usage: `streamBody(PekkoStreams)(schema, format)`. The capability can be added to the classpath independently of the interpreter through the `"com.softwaremill.sttp.shared" %% "pekko"` dependency. ## Web sockets The interpreter supports web sockets, with pipes of type `Flow[REQ, RESP, Any]`. See [web sockets](../endpoint/websockets.md) for more details. pekko-http does not expose control frames (`Ping`, `Pong` and `Close`), so any setting regarding them are discarded, and ping/pong frames which are sent explicitly are ignored. [Automatic pings](https://pekko.apache.org/docs/pekko-http/current/server-side/websocket-support.html#automatic-keep-alive-ping-support) can be instead enabled through configuration. ## Server Sent Events The interpreter supports [SSE (Server Sent Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). For example, to define an endpoint that returns event stream: ```scala import org.apache.pekko.stream.scaladsl.Source import sttp.model.sse.ServerSentEvent import sttp.tapir.* import sttp.tapir.server.pekkohttp.{PekkoHttpServerInterpreter, serverSentEventsBody} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global val sseEndpoint = endpoint.get.out(serverSentEventsBody) val routes = PekkoHttpServerInterpreter().toRoute(sseEndpoint.serverLogicSuccess[Future](_ => Future.successful(Source.single(ServerSentEvent(Some("data"), None, None, None))) )) ``` ## Configuration The interpreter can be configured by providing an `PekkoHttpServerOptions` value, see [server options](options.md) for details. # Running as a Play server Tapir supports both Play 2.9, which still ships with Akka, and Play 3.0, which replaces Akka with Pekko. See the [Play framework documentation](https://www.playframework.com/documentation/2.9.x/General#How-Play-Deals-with-Akkas-License-Change) for differences between these versions. To expose an endpoint as a [play-server](https://www.playframework.com/), using **Play 2.9 with Akka**, add the following dependencies: ```scala "com.softwaremill.sttp.tapir" %% "tapir-play29-server" % "1.13.31" ``` and (if you don't already depend on Play) ```scala "org.playframework" %% "play-akka-http-server" % "2.9.11" ``` or ```scala "org.playframework" %% "play-netty-server" % "2.9.11" ``` depending on whether you want to use netty or Akka based http-server under the hood. Please note that Play 2.9 server is available only for Scala 2.13. To expose an endpoint as a [play-server](https://www.playframework.com/), using **Play 3.0 with Pekko**, add the following dependencies: ```scala "com.softwaremill.sttp.tapir" %% "tapir-play-server" % "1.13.31" ``` and (if you don't already depend on Play) ```scala "org.playframework" %% "play-pekko-http-server" % "3.0.11" ``` or ```scala "org.playframework" %% "play-netty-server" % "3.0.11" ``` depending on whether you want to use netty or Pekko based http-server under the hood. The following code samples use **Play 3.0 with Pekko**. If you are using Play 2.9 with Akka, simply replace the import of `org.apache.pekko.stream.Materializer` with `import akka.stream.Materializer`. Then import the object: ```scala import sttp.tapir.server.play.PlayServerInterpreter ``` The `toRoutes` method requires a single, or a list of `ServerEndpoint`s, which can be created by adding [server logic](logic.md) to an endpoint. For example: ```scala import org.apache.pekko.stream.Materializer import play.api.routing.Router.Routes import sttp.tapir.* import sttp.tapir.server.play.PlayServerInterpreter import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future given Materializer = ??? def countCharacters(s: String): Future[Either[Unit, Int]] = Future(Right[Unit, Int](s.length)) val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) val countCharactersRoutes: Routes = PlayServerInterpreter().toRoutes(countCharactersEndpoint.serverLogic(countCharacters _)) ``` ```{note} A single Play application can contain both tapir-managed and Play-managed routes. However, because of the routing implementation in Play, the shape of the paths that tapir and other Play handlers serve should not overlap. The shape of the path includes exact path segments, single- and multi-wildcards. Otherwise, request handling will throw an exception. We don't expect users to encounter this as a problem, however the implementation here diverges a bit comparing to other interpreters. ``` ## Bind the routes ### Creating the HTTP server manually An HTTP server can then be started as in the following example: ```scala import play.core.server.* import play.api.routing.Router.Routes val aRoute: Routes = ??? // JVM entry point that starts the HTTP server - uncomment @main to run /* @main */ def playServer(): Unit = val playConfig = ServerConfig(port = sys.props.get("http.port").map(_.toInt).orElse(Some(9000)) ) NettyServer.fromRouterWithComponents(playConfig) { components => aRoute } ``` ### As part of an existing Play application Or, if you already have an existing Play application, you can create a `Router` class and bind it to the application. First, add a line like following in the `routes` files: ``` -> /api api.ApiRouter ``` Then create a class like this: ```scala class ApiRouter @Inject() () extends SimpleRouter: override def routes: Routes = anotherRoutes.orElse(tapirGeneratedRoutes) ``` Find more details about how to bind a `Router` to your application in the [Play framework documentation](https://www.playframework.com/documentation/2.8.x/ScalaSirdRouter#Binding-sird-Router). ## Web sockets The interpreter supports web sockets, with pipes of type `Flow[REQ, RESP, Any]`. See [web sockets](../endpoint/websockets.md) for more details. The interpreter does not expose control frames (`Ping`, `Pong` and `Close`), so any setting regarding them are discarded, however those that are emitted are sent to the client. ## Configuration The interpreter can be configured by providing a `PlayServerOptions` value, see [server options](options.md) for details. # Running as a Vert.X server Endpoints can be mounted as Vert.x `Route`s on top of a Vert.x `Router`. Vert.x interpreter can be used with different effect systems (cats-effect, ZIO) as well as Scala's standard `Future`. ## Scala's standard `Future` Add the following dependency ```scala "com.softwaremill.sttp.tapir" %% "tapir-vertx-server" % "1.13.31" ``` to use this interpreter with `Future`. Then import the object: ```scala import sttp.tapir.server.vertx.VertxFutureServerInterpreter.* ``` This object contains the following methods: * `route(e: ServerEndpoint[Any, Future])`: returns a function `Router => Route` that will create a route with a handler attached, matching the endpoint definition. Errors will be recovered automatically (but generically) * `blockingRoute(e: ServerEndpoint[Any, Future])`: returns a function `Router => Route` that will create a route with a blocking handler attached, matching the endpoint definition. Errors will be recovered automatically (but generically) In practice, routes will be mounted on a router, this router can then be used as a request handler for your http server. An HTTP server can then be started as in the following example: ```scala import sttp.tapir.* import sttp.tapir.server.vertx.VertxFutureServerInterpreter import sttp.tapir.server.vertx.VertxFutureServerInterpreter.* import io.vertx.core.Vertx import io.vertx.ext.web.* import scala.concurrent.{Await, Future} import scala.concurrent.duration.* // JVM entry point that starts the HTTP server - uncomment @main to run /* @main */ def vertxServer(): Unit = val vertx = Vertx.vertx() val server = vertx.createHttpServer() val router = Router.router(vertx) val anEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? // your definition here def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? // your logic here val attach = VertxFutureServerInterpreter().route(anEndpoint.serverLogic((logic _).tupled)) attach(router) // your endpoint is now attached to the router, and the route has been created Await.result(server.requestHandler(router).listen(9000).asScala, Duration.Inf) ``` ## Configuration Every endpoint can be configured by providing an instance of `VertxFutureEndpointOptions`, see [server options](options.md) for details. You can also provide your own `ExecutionContext` to execute the logic. ## Defining an endpoint together with the server logic It's also possible to define an endpoint together with the server logic in a single, more concise step. See [server logic](logic.md) for details. ## Cats Effect typeclasses Add the following dependency ```scala "com.softwaremill.sttp.tapir" %% "tapir-vertx-server-cats" % "1.13.31" ``` to use this interpreter with Cats Effect typeclasses. Then import the object: ```scala import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter.* ``` This object contains the `route[F[_]](e: ServerEndpoint[Fs2Streams[F], F])` method, which returns a function `Router => Route` that will create a route, with a handler attached, matching the endpoint definition. Errors will be recovered automatically. Here is simple example which starts HTTP server with one route: ```scala import cats.effect.* import cats.effect.std.Dispatcher import io.vertx.core.Vertx import io.vertx.ext.web.Router import sttp.tapir.* import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter.* object App extends IOApp: val responseEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint .in("response") .in(query[String]("key")) .out(plainBody[String]) def handler(req: String): IO[Either[Unit, String]] = IO.pure(Right(req)) override def run(args: List[String]): IO[ExitCode] = Dispatcher[IO] .flatMap { dispatcher => Resource .make( IO.delay { val vertx = Vertx.vertx() val server = vertx.createHttpServer() val router = Router.router(vertx) val attach = VertxCatsServerInterpreter[IO](dispatcher).route(responseEndpoint.serverLogic(handler)) attach(router) server.requestHandler(router).listen(8080) }.flatMap(_.asF[IO]) )({ server => IO.delay(server.close).flatMap(_.asF[IO].void) }) } .use(_ => IO.never) ``` This interpreter also supports streaming using FS2 streams: ```scala import cats.effect.* import cats.effect.std.Dispatcher import fs2.* import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.* import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter val streamedResponse = endpoint .in("stream") .in(query[Int]("key")) .out(streamTextBody(Fs2Streams[IO])(CodecFormat.TextPlain())) def dispatcher: Dispatcher[IO] = ??? val attach = VertxCatsServerInterpreter(dispatcher).route(streamedResponse.serverLogicSuccess[IO] { key => IO.pure(Stream.chunk(Chunk.array("Hello world!".getBytes)).repeatN(key)) }) ``` ## ZIO Add the following dependency ```scala "com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio" % "1.13.31" ``` to use this interpreter with ZIO. Then import the object: ```scala import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.* ``` This object contains method `def route(e: ServerEndpoint[ZioStreams, RIO[R, *]])` which returns a function `Router => Route` that will create a route matching the endpoint definition, and with the logic attached as a handler. Here is simple example which starts HTTP server with one route: ```scala import io.vertx.core.Vertx import io.vertx.ext.web.Router import sttp.tapir.{plainBody, query} import sttp.tapir.ztapir.* import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.* import zio.* object Short extends ZIOAppDefault: override implicit val runtime = zio.Runtime.default val responseEndpoint = endpoint .in("response") .in(query[String]("key")) .out(plainBody[String]) val attach = VertxZioServerInterpreter().route(responseEndpoint.zServerLogic { key => ZIO.succeed(key) }) override def run = ZIO.scoped( ZIO .acquireRelease( ZIO .attempt { val vertx = Vertx.vertx() val server = vertx.createHttpServer() val router = Router.router(vertx) attach(router) server.requestHandler(router).listen(8080) } .flatMap(_.asRIO) ) { server => ZIO.attempt(server.close()).flatMap(_.asRIO).orDie } *> ZIO.never ) ``` This interpreter supports streaming using ZStreams. # Running as an http4s server using ZIO The `tapir-zio` module defines type aliases and extension methods which make it more ergonomic to work with [ZIO](https://zio.dev) and tapir. Moreover, `tapir-zio-http4s-server` contains an interpreter useful when exposing the endpoints using the [http4s](https://http4s.org) server. The `*-zio` modules depend on ZIO 2.x. You'll need the following dependency for the `ZServerEndpoint` type alias and helper classes: ```scala "com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.13.31" ``` or just add the zio-http4s integration which already depends on `tapir-zio`: ```scala "com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "1.13.31" ``` Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): ```scala import sttp.tapir.ztapir.* ``` This brings into scope all of the [basic](../endpoint/basics.md) input/output descriptions, which can be used to define an endpoint. ```{note} You should have only one of these imports in your source file. Otherwise, you'll get naming conflicts. The `import sttp.tapir.ztapir.*` import is meant as a complete replacement of `import sttp.tapir.*`. ``` ## Server logic When defining the business logic for an endpoint, the following methods are available, which replace the [standard ones](logic.md): * `def zServerLogic[R](logic: I => ZIO[R, E, O]): ZServerEndpoint[R, C]` for public endpoints * `def zServerSecurityLogic[R, U](f: A => ZIO[R, E, U]): ZPartialServerEndpoint[R, A, U, I, E, O, C]` for secure endpoints The first defines complete server logic, while the second allows defining first the security server logic, and then the rest. ```{note} When using Scala 3, it's best to provide the type of the environment explicitly to avoid type inferencing issues. E.g.: `myEndpoint.zServerLogic[Any](...)`. ``` ## Exposing endpoints using the http4s server To interpret a `ZServerEndpoint` as an http4s server, use the following interpreter: ```scala import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter ``` To help with type-inference, you first need to call `ZHttp4sServerInterpreter().from()` providing: * a single server endpoint: `def from[I, E, O, C](se: ZServerEndpoint[R, I, E, O, C])` * multiple server endpoints: `def from[C](serverEndpoints: List[ZServerEndpoint[R, _, _, _, C]])` Then, call `.toRoutes` to obtain the http4s `HttpRoutes` instance. Note that the resulting `HttpRoutes` always requires `Clock` in the environment. If you have multiple endpoints with different environmental requirements, the environment must be first widened so that it is uniform across all endpoints, using the `.widen` method: ```scala import org.http4s.HttpRoutes import sttp.tapir.ztapir.* import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import zio.RIO trait Component1 trait Component2 type Service1 = Component1 type Service2 = Component2 val serverEndpoint1: ZServerEndpoint[Service1, Any] = ??? val serverEndpoint2: ZServerEndpoint[Service2, Any] = ??? type Env = Service1 with Service2 val routes: HttpRoutes[RIO[Env, *]] = ZHttp4sServerInterpreter().from(List( serverEndpoint1.widen[Env], serverEndpoint2.widen[Env] )).toRoutes // this is where zio-cats interop is needed ``` ## Streaming The http4s interpreter accepts streaming bodies of type `zio.stream.Stream[Throwable, Byte]`, as described by the `ZioStreams` capability. Both response bodies and request bodies can be streamed. Usage: `streamBody(ZioStreams)(schema, format)`. The capability can be added to the classpath independently of the interpreter through the `"com.softwaremill.sttp.shared" %% "zio"` or `tapir-zio` dependency. ## Http4s backends Http4s integrates with a couple of [server backends](https://http4s.org/v1.0/integrations/), the most popular being Blaze and Ember. In the [examples](../examples.md) and throughout the docs we use Blaze, but other backends can be used as well. This means adding another dependency, such as: ```scala "org.http4s" %% "http4s-blaze-server" % Http4sVersion ``` ## Web sockets The interpreter supports web sockets, with pipes of type `zio.stream.Stream[Throwable, REQ] => zio.stream.Stream[Throwable, RESP]`. See [web sockets](../endpoint/websockets.md) for more details. However, endpoints which use web sockets need to be interpreted using the `ZHttp4sServerInterpreter.fromWebSocket` method. This can then be added to a server builder using `withHttpWebSocketApp`, for example: ```scala import sttp.capabilities.WebSockets import sttp.capabilities.zio.ZioStreams import sttp.tapir.{CodecFormat, PublicEndpoint} import sttp.tapir.ztapir.* import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router import org.http4s.server.websocket.WebSocketBuilder2 import scala.concurrent.ExecutionContext import zio.{Task, Runtime, ZIO} import zio.interop.catz.* import zio.stream.Stream def runtime: Runtime[Any] = ??? // provided by ZIOAppDefault given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global val wsEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, String] => Stream[Throwable, String], ZioStreams with WebSockets] = endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](ZioStreams)) val wsRoutes: WebSocketBuilder2[Task] => HttpRoutes[Task] = ZHttp4sServerInterpreter().fromWebSocket(wsEndpoint.zServerLogic(_ => ???)).toRoutes val serve: Task[Unit] = ZIO.executor.flatMap(executor => BlazeServerBuilder[Task] .withExecutionContext(executor.asExecutionContext) .bindHttp(8080, "localhost") .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) .serve .compile .drain ) ``` ## Server Sent Events The interpreter supports [SSE (Server Sent Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). For example, to define an endpoint that returns event stream: ```scala import sttp.capabilities.zio.ZioStreams import sttp.model.sse.ServerSentEvent import sttp.tapir.server.http4s.ztapir.{ZHttp4sServerInterpreter, serverSentEventsBody} import sttp.tapir.PublicEndpoint import sttp.tapir.ztapir.* import org.http4s.HttpRoutes import zio.{Task, ZIO} import zio.stream.{Stream, ZStream} val sseEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, ServerSentEvent], ZioStreams] = endpoint.get.out(serverSentEventsBody) val routes: HttpRoutes[Task] = ZHttp4sServerInterpreter() .from(sseEndpoint.zServerLogic(_ => ZIO.succeed(ZStream(ServerSentEvent(Some("data"), None, None, None))))) .toRoutes ``` ## Examples There's a couple of [examples](../examples.md) of using the ZIO integration available. # Running as a zio-http server The `tapir-zio` module defines type aliases and extension methods which make it more ergonomic to work with [ZIO](https://zio.dev) and tapir. Moreover, `tapir-zio-http-server` contains an interpreter useful when exposing the endpoints using the [ZIO Http](https://github.com/dream11/zio-http) server. The `*-zio` modules depend on ZIO 2.x. You'll need the following dependency for the `ZServerEndpoint` type alias and helper classes: ```scala "com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.13.31" ``` or just add the zio-http integration which already depends on `tapir-zio`: ```scala "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "1.13.31" ``` Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): ```scala import sttp.tapir.ztapir.* ``` This brings into scope all the [basic](../endpoint/basics.md) input/output descriptions, which can be used to define an endpoint. ```{note} You should have only one of these imports in your source file. Otherwise, you'll get naming conflicts. The `import sttp.tapir.ztapir.*` import is meant as a complete replacement of `import sttp.tapir.*`. ``` ## Exposing endpoints ```scala import sttp.tapir.server.ziohttp.ZioHttpInterpreter ``` The `ZioHttpInterpreter` objects contains the `toHttp` method, which requires a `ZServerEndpoint` (see below). For example: ```scala import sttp.tapir.PublicEndpoint import sttp.tapir.ztapir.* import sttp.tapir.server.ziohttp.ZioHttpInterpreter import zio.http.{Request, Response, Routes} import zio.* def countCharacters(s: String): ZIO[Any, Nothing, Int] = ZIO.succeed(s.length) val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] = endpoint.in(stringBody).out(plainBody[Int]) val countCharactersHttp: Routes[Any, Response] = ZioHttpInterpreter().toHttp(countCharactersEndpoint.zServerLogic(countCharacters)) ``` ```{note} A single ZIO Http application can contain both Tapir-generated and ZIO-Http-native routes. However, because of the routing implementation in ZIO Http, the shape of the paths that Tapir and other ZIO Http routes serve should not overlap. The shape of the path includes exact path segments, single- and multi-wildcards. Such overlapping routes may cause incorrect 404 (Not Found) or 405 (Method Not Allowed) responses. ``` ```{note} Middleware which changes the number of path segments in a request, such as `HandlerAspect.updatePath`, should not be applied to Tapir-generated routes. Tapir matches the path against the endpoint definitions itself, using the path that ZIO Http matched when routing. If that path is changed afterwards, requests will either not match any endpoint, or fail with a 500 (Internal Server Error). ``` ## Server logic When defining the business logic for an endpoint, the following methods are available, which replace the [standard ones](logic.md): * `def zServerLogic[R](logic: I => ZIO[R, E, O]): ZServerEndpoint[R, C]` for public endpoints * `def zServerSecurityLogic[R, U](f: A => ZIO[R, E, U]): ZPartialServerEndpoint[R, A, U, I, E, O, C]` for secure endpoints The first defines complete server logic, while the second and third allow defining server logic in parts. ```{note} When using Scala 3, it's best to provide the type of the environment explicitly to avoid type inferencing issues. E.g.: `myEndpoint.zServerLogic[Any](...)`. ``` ## Streaming The zio-http interpreter accepts streaming bodies of type `Stream[Throwable, Byte]`, as described by the `ZioStreams` capability. Both response bodies and request bodies can be streamed. Usage: `streamBody(ZioStreams)(schema, format)`. The capability can be added to the classpath independently of the interpreter through the `"com.softwaremill.sttp.shared" %% "zio"` dependency. ## Web sockets The interpreter supports web sockets, with pipes of type `zio.stream.Stream[Throwable, REQ] => zio.stream.Stream[Throwable, RESP]`. See [web sockets](../endpoint/websockets.md) for more details. It also supports auto-ping, auto-pong-on-ping, ignoring-pongs and handling of fragmented frames. ## Error handling By default, any endpoints interpreted with the `ZioHttpInterpreter` will use tapir's built-in failed effect handling, which uses an interceptor. Errors can be sent in a custom format by [providing a custom `ErrorHandler`](errors.md). If you'd prefer to use zio-http's error handling, you can disable tapir's exception interceptor by modifying the [server options](options.md): ```scala import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions} ZioHttpInterpreter(ZioHttpServerOptions.customiseInterceptors[Any].exceptionHandler(None).options) ``` ## Configuration The interpreter can be configured by providing an `ZioHttpServerOptions` value, see [server options](options.md) for details. # Using as an sttp client (v3) Add the dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "1.13.31" ``` To make requests using an endpoint definition using the [sttp client](https://github.com/softwaremill/sttp), import: ```scala import sttp.tapir.client.sttp.SttpClientInterpreter ``` This object contains a number of variants for creating a client call, where the first parameter is the endpoint description. The second is an optional URI - if this is `None`, the request will be relative. Here's a summary of the available method variants; `R` are the requirements of the endpoint, such as streaming or websockets: - `toRequest(PublicEndpoint, Option[Uri])`: returns a function, which represents decoding errors as the `DecodeResult` class. ```scala I => Request[DecodeResult[Either[E, O]], R] ``` After providing the input parameters, a description of the request to be made is returned, with the input value encoded as appropriate request parameters: path, query, headers and body. This can be further customised and sent using any sttp backend. The response will then contain the decoded error or success values (note that this can be the body enriched with data from headers/status code). - `toRequestThrowDecodeFailures(PublicEndpoint, Option[Uri])`: returns a function, which will throw an exception or return a failed effect if decoding of the result fails ```scala I => Request[Either[E, O], R] ``` - `toRequestThrowErrors(PublicEndpoint, Option[Uri])`: returns a function, which will throw an exception or return a failed effect if decoding of the result fails, or if the result is an error (as described by the endpoint) ```scala I => Request[O, R] ``` Next, there are `toClient(PublicEndpoint, Option[Uri], SttpBackend[F, R])` methods (in the above variants), which send the request using the given backend. Hence in this case, the signature of the result is: ```scala I => F[DecodeResult[Either[E, O]]] ``` Finally, for secure endpoints, there are `toSecureClient` and `toSecureRequest` families of methods. They return functions which first accept the security inputs, and then the regular inputs. For example: ```scala // toSecureRequest(Endpoint, Option[Uri]) returns: A => I => Request[DecodeResult[Either[E, O]], R] // toSecureClient(Endpoint, Option[Uri], SttpBackend) returns: A => I => F[DecodeResult[Either[E, O]]] ``` See the [runnable example](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/booksExample.scala) for example usage. ## Web sockets To interpret a web socket endpoint, an additional streams-specific import is needed, so that the interpreter can convert sttp's `WebSocket` instance into a pipe. This logic is looked up via the `WebSocketToPipe` implicit. The required imports are as follows: ```scala import sttp.tapir.client.sttp.ws.pekkohttp.* // for pekko-streams import sttp.tapir.client.sttp.ws.akkahttp.* // for akka-streams import sttp.tapir.client.sttp.ws.fs2.* // for fs2 import sttp.tapir.client.sttp.ws.zio.* // for zio ``` No additional dependencies are needed, as both of the above implementations are included in the main interpreter, with dependencies on pekko-streams, akka-streams, fs2 and zio being marked as optional (hence these are not transitive). ## Overwriting the response specification The `Request` obtained from the `.toRequest` and `.toSecureRequest` families of methods, after being applied to the input, contains both the request data (URI, headers, body), and a description of how to handle the response - depending on the variant used, decoding the response into one of endpoint's outputs. If you'd like to skip that step, e.g. when testing redirects, it's possible to overwrite the response handling description, for example: ```scala :compile-only import sttp.tapir.* import sttp.tapir.client.sttp4.SttpClientInterpreter import sttp.client4.* SttpClientInterpreter() .toRequest(endpoint.get.in("hello").in(query[String]("name")), Some(uri"http://localhost:8080")) .apply("Ann") .response(asStringAlways) ``` ## Scala.JS In this case add the following dependencies (note the [`%%%`](https://www.scala-js.org/doc/project/dependencies.html) instead of the usual `%%`): ```scala "com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % "1.13.31" "io.github.cquiroz" %%% "scala-java-time" % "2.2.0" // implementations of java.time classes for Scala.JS ``` The client interpreter also supports Scala.JS, the request must then be sent using the [sttp client Scala.JS Fetch backend](https://sttp.softwaremill.com/en/latest/backends/javascript/fetch.html). You can check the [`SttpClientTests`](https://github.com/softwaremill/tapir/blob/master/client/sttp-client/src/test/scalajs/sttp/tapir/client/sttp/SttpClientTests.scala) for a working example. ## Limitations There are limitations existing on some clients that prevent the description generated by tapir from being decoded correctly. For security reasons the [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) header is not accessible from frontend JavaScript code. It means that any endpoint containing a `.out(setCookie("token"))` will fail to be decoded on the client side when using Fetch. A solution is to use the `setCookieOpt` function instead an let the browser do its job when dealing with cookies. # Using as an http4s client Add the dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-http4s-client" % "1.13.31" ``` To interpret an endpoint definition as an `org.http4s.Request[F]`, import: ```scala import sttp.tapir.client.http4s.Http4sClientInterpreter ``` This objects contains four methods: - `toRequestThrowDecodeFailures(PublicEndpoint, Option[Uri])` and `toSecureRequestThrowDecodeFailures(Endpoint, Option[Uri])`: given the optional base URI, returns a function which generates a request and a response parser from endpoint inputs. Response parser throws an exception if decoding of the result fails. ```scala I => (org.http4s.Request[F], org.http4s.Response[F] => F[Either[E, O]]) ``` - `toRequest(PublicEndpoint, Option[Uri])` and `toSecureRequest(Endpoint, Option[Uri])`: given the optional base URI, returns a function which generates a request and a response parser from endpoint inputs. Response parser returns an instance of `DecodeResult` which contains the decoded response body or error details. ```scala I => (org.http4s.Request[F], org.http4s.Response[F] => F[DecodeResult[Either[E, O]]]) ``` Note that the returned functions have one argument each: first the security inputs (if any), and regular input values of the endpoint. This might be a single type, a tuple, or a case class, depending on the endpoint description. After providing the input parameters, the following values are returned: - An instance of `org.http4s.Request[F]` with the input value encoded as appropriate request parameters: path, query, headers and body. The request can be further customised and sent using an http4s client, or run against `org.http4s.HttpRoutes[F]`. - A response parser to be applied to the response received after executing the request. The result will then contain the decoded error or success values (note that this can be the body enriched with data from headers/status code). See the [runnable example](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala) for example usage. ## Limitations - Multipart requests are not supported yet. - WebSockets are not supported yet. - Streaming capabilities: - only `Fs2Streams` are supported at the moment. # Using as a Play client Tapir supports both Play 2.9, which still ships with Akka, and Play 3.0, which replaces Akka with Pekko. See the [Play framework documentation](https://www.playframework.com/documentation/2.9.x/General#How-Play-Deals-with-Akkas-License-Change) for differences between these versions. For **Play 3.0**, add the dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-play-client" % "1.13.31" ``` For **Play 2.9**, add ```scala "com.softwaremill.sttp.tapir" %% "tapir-play29-client" % "1.13.31" ``` instead. Furthermore, replace all uses of `sttp.capabilities.pekko.PekkoStreams` in the following code snippets with `sttp.capabilities.akka.AkkaStreams`. To make requests using an endpoint definition using the [play client](https://github.com/playframework/play-ws), import: ```scala import sttp.tapir.client.play.PlayClientInterpreter ``` This objects contains four methods: - `toRequestThrowDecodeFailures(PublicEndpoint, String)` and `toSecureRequestThrowDecodeErrors(Endpoint, String)`: given the base URI returns a function, which will generate a request and a response parser which might throw an exception when decoding of the result fails ```scala I => (StandaloneWSRequest, StandaloneWSResponse => Either[E, O]) ``` - `toRequest(PublicEndpoint, String)` and `toSecureRequest(Endpoint, String)`: given the base URI returns a function, which will generate a request and a response parser which represents decoding errors as the `DecodeResult` class ```scala I => (StandaloneWSRequest, StandaloneWSResponse => DecodeResult[Either[E, O]]) ``` Note that the returned functions have one argument each: first the security inputs (if any), and regular input values of the endpoint. This might be a single type, a tuple, or a case class, depending on the endpoint description. After providing the input parameters, the two following are returned: - a description of the request to be made, with the input value encoded as appropriate request parameters: path, query, headers and body. This can be further customised and sent using regular Play methods. - a response parser to be applied to the response got after executing the request. The result will then contain the decoded error or success values (note that this can be the body enriched with data from headers/status code). Example: ```scala import sttp.tapir.* import sttp.tapir.client.play.PlayClientInterpreter import sttp.capabilities.pekko.PekkoStreams import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import play.api.libs.ws.StandaloneWSClient def example[I, E, O, R >: PekkoStreams](implicit wsClient: StandaloneWSClient): Unit = val e: PublicEndpoint[I, E, O, R] = ??? val inputArgs: I = ??? val (req, responseParser) = PlayClientInterpreter() .toRequestThrowDecodeFailures(e, s"http://localhost:9000") .apply(inputArgs) val result: Future[Either[E, O]] = req .execute() .map(responseParser) ``` ## Limitations Multipart requests are not supported. Streaming capabilities: - only `PekkoStreams` is supported (resp. `AkkaStreams` for Play 2.9) # Using as an sttp client (v4) Add the dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-sttp-client4" % "1.13.31" ``` To make requests using an endpoint definition using the [sttp client](https://github.com/softwaremill/sttp), import: ```scala import sttp.tapir.client.sttp4.SttpClientInterpreter ``` This object contains a number of variants for creating a client call, where the first parameter is the endpoint description. The second is an optional URI - if this is `None`, the request will be relative. ```{note} If you've been using sttp client3, there have been some changes in the API. Notably, describing an HTTP request yields different request types, depending on the backend capabilities that are required to send the request. Hence, there's a `Request`, `StreamRequest` and `WebSocketRequest` type instead of a single `RequestT`. For a more detailed description of the changes, refer to sttp client's [migration docs](https://sttp.softwaremill.com/en/latest/migrate_v3_v4.html). ``` Here's a summary of the available method variants. Note that the below will work only for endpoints which do not require any additional capabilities (such as streaming or websockets), hence where the `R` type parameter is `Any`. For other endpoints, see the sections on streaming and websockets below. - `toRequest(PublicEndpoint, Option[Uri])`: returns a function, which represents decoding errors as the `DecodeResult` class. ```scala I => Request[DecodeResult[Either[E, O]]] ``` After providing the input parameters, a description of the request to be made is returned, with the input value encoded as appropriate request parameters: path, query, headers and body. This can be further customized and sent using an sttp backend. The response will contain the decoded error or success values (note that this can be the body enriched with data from headers/status code). - `toRequestThrowDecodeFailures(PublicEndpoint, Option[Uri])`: returns a function, which will throw an exception or return a failed effect if decoding of the result fails ```scala I => Request[Either[E, O]] ``` - `toRequestThrowErrors(PublicEndpoint, Option[Uri])`: returns a function, which will throw an exception or return a failed effect if decoding of the result fails, or if the result is an error (as described by the endpoint) ```scala I => Request[O] ``` Next, there are `toClient(PublicEndpoint, Option[Uri], Backend[F])` methods (with analogous variants), which send the request using the given backend. Hence in this case, the signature of the result is: ```scala I => F[DecodeResult[Either[E, O]]] ``` Finally, for secure endpoints, there are `toSecureClient` and `toSecureRequest` families of methods. They return functions which first accept the security inputs, and then the regular inputs. For example: ```scala // toSecureRequest(Endpoint, Option[Uri]) returns: A => I => Request[DecodeResult[Either[E, O]]] // toSecureClient(Endpoint, Option[Uri], SttpBackend) returns: A => I => F[DecodeResult[Either[E, O]]] ``` ## Streaming To interpret a streaming endpoint, you'll need to use a different import: ```scala import sttp.tapir.client.sttp4.stream.StreamSttpClientInterpreter ``` The `StreamSttpClientInterpreter` contains method analogous to the ones in the "basic" `SttpClientInterpreter`. The difference is that the streaming interpreter works only for endpoints, which require the streaming capability: that is, their `R` type parameter must be a subtype of `sttp.capabilities.Streams[_]`. Moreover, the result type the request-creating methods is a `StreamRequest`, instead of a `Request`. ## Web sockets To interpret a web socket endpoint, an additional streams-specific import is needed, so that the interpreter can convert sttp's `WebSocket` instance into a pipe. This logic is looked up via the `WebSocketToPipe` implicit. The required imports are as follows: ```scala import sttp.tapir.client.sttp4.ws.WebSocketSttpClientInterpreter // mandatory import sttp.tapir.client.sttp4.ws.pekkohttp.* // for pekko-streams import sttp.tapir.client.sttp4.ws.fs2.* // for fs2 import sttp.tapir.client.sttp4.ws.zio.* // for zio ``` No additional dependencies are needed, as both of the above implementations are included in the main interpreter, with dependencies on pekko-streams, fs2 and zio being marked as optional (hence these are not transitive). Just as with streaming, the request types returned by the `WebSocketSttpClientInterpreter` are of type `WebSocketRequest`, instead of `Request`. ## Overwriting the response specification The `Request` obtained from the `.toRequest` and `.toSecureRequest` families of methods, after being applied to the input, contains both the request data (URI, headers, body), and a description of how to handle the response - depending on the variant used, decoding the response into one of endpoint's outputs. If you'd like to skip that step, e.g. when testing redirects, it's possible to overwrite the response handling description, for example: ```scala import sttp.tapir.* import sttp.tapir.client.sttp4.SttpClientInterpreter import sttp.client4.* SttpClientInterpreter() .toRequest(endpoint.get.in("hello").in(query[String]("name")), Some(uri"http://localhost:8080")) .apply("Ann") .response(asStringAlways) ``` ## Scala.JS In this case add the following dependencies (note the [`%%%`](https://www.scala-js.org/doc/project/dependencies.html) instead of the usual `%%`): ```scala "com.softwaremill.sttp.tapir" %%% "tapir-sttp-client4" % "1.13.31" "io.github.cquiroz" %%% "scala-java-time" % "2.2.0" // implementations of java.time classes for Scala.JS ``` The client interpreter also supports Scala.JS, the request must then be sent using the [sttp client Scala.JS Fetch backend](https://sttp.softwaremill.com/en/latest/backends/javascript/fetch.html). ## Limitations There are limitations existing on some clients that prevent the description generated by Tapir from being decoded correctly. For security reasons the [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) header is not accessible from frontend JavaScript code. It means that any endpoint containing a `.out(setCookie("token"))` will fail to be decoded on the client side when using Fetch. A solution is to use the `setCookieOpt` function instead an let the browser do its job when dealing with cookies. # Generating OpenAPI documentation To expose documentation, endpoints first need to be interpreted into an OpenAPI yaml or json. Then, the generated description of our API can be exposed using a UI such as Swagger or Redoc. These two operations can be done in a single step, using the `SwaggerInterpreter` or `RedocInterpreter`. Or, if needed, these steps can be done separately, giving you complete control over the process. ## Generating and exposing documentation in a single step ### Using Swagger To generate OpenAPI documentation and expose it using the [Swagger UI](https://github.com/swagger-api/swagger-ui) in a single step, first add the dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.13.31" ``` Then, you can interpret a list of endpoints using `SwaggerInterpreter`. The result will be a list of file-serving server endpoints, which use the yaml corresponding to the endpoints passed originally. These swagger endpoints, together with the endpoints for which the documentation is generated, will need in turn to be interpreted using your server interpreter. For example: ```scala import sttp.tapir.* import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.tapir.server.netty.{NettyFutureServerInterpreter, FutureRoute} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global val myEndpoints: List[AnyEndpoint] = ??? // first interpret as swagger ui endpoints, backend by the appropriate yaml val swaggerEndpoints = SwaggerInterpreter().fromEndpoints[Future](myEndpoints, "My App", "1.0") // add to your netty routes val swaggerRoute: FutureRoute = NettyFutureServerInterpreter().toRoute(swaggerEndpoints) ``` By default, the documentation will be available under the `/docs` path. The path, as well as other options can be changed when creating the `SwaggerInterpreter` and invoking `fromEndpoints`. If the Swagger UI endpoints are deployed within a context, and you don't want Swagger to use relative paths, you'll need to set the `useRelativePaths` options to `false`, and specify the `contextPath` one. Moreover, model generation can be configured - see below for more details on `OpenAPIDocsOptions` and the method parameters of `fromEndpoints`. Finally, the generated model can be customised. See the scaladocs for `SwaggerInterpreter`. The swagger server endpoints can be secured using `ServerLogic.prependSecurity`, see [server logic](../server/logic.md) for details. ### Using Redoc For [Redoc](https://github.com/redocly/redoc), you'll need the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.13.31" ``` And the server endpoints can be generated using the `sttp.tapir.redoc.bundle.RedocInterpreter` class. ### Using Scalar For [Scalar](https://github.com/scalar/scalar), you'll need the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-scalar-bundle" % "1.13.31" ``` And the server endpoints can be generated using the `sttp.tapir.scalar.bundle.ScalarInterpreter` class. ## Generating OpenAPI documentation separately To generate the docs in the OpenAPI yaml format, add the following dependencies: ```scala "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.13.31" "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` The case-class based model of the openapi data structures is present in the [sttp-apispec](https://github.com/softwaremill/sttp-apispec) project. An endpoint can be converted to an instance of the model by importing the `sttp.tapir.docs.openapi.OpenAPIDocsInterpreter` object: ```scala import sttp.apispec.openapi.OpenAPI import sttp.tapir.* 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](https://github.com/adamw/quicklens). The documentation is generated in a large part basing on [schemas](../endpoint/schemas.md). 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: ```scala import sttp.apispec.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: ```scala OpenAPIDocsInterpreter().toOpenAPI(List(addBook, booksListing, booksListingByGenre), "My Bookshop", "1.0") ``` The openapi case classes can then be serialised to YAML using [Circe](https://circe.github.io/circe/): ```scala import sttp.apispec.openapi.circe.yaml.* println(docs.toYaml) ``` Or to JSON: ```scala import io.circe.Printer import io.circe.syntax.* import sttp.apispec.openapi.circe.* println(Printer.spaces2.print(docs.asJson)) ``` ### Support for OpenAPI 3.0.3 Generating OpenAPI documentation compatible with 3.0.3 specifications is a matter of using a different encoder. For example, generating the OpenAPI 3.0.3 YAML string can be achieved by performing the following steps: Firstly add dependencies: ```scala "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.13.31" "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` and generate the documentation by importing valid extension methods and explicitly specifying the "3.0.3" version in the OpenAPI model: ```scala import sttp.apispec.openapi.OpenAPI import sttp.apispec.openapi.circe.yaml.* // for `toYaml` extension method import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter case class Book(id: Option[Long], title: Option[String]) val booksListing = endpoint.in(path[String]("bookId")) val docs: OpenAPI = OpenAPIDocsInterpreter().toOpenAPI(booksListing, "My Bookshop", "1.0").openapi("3.0.3") // "3.0.3" version explicitly specified println(docs.toYaml3_0_3) // OpenApi 3.0.3 YAML string would be printed to the console ``` ## Exposing generated OpenAPI documentation Exposing the OpenAPI can be done using [Swagger UI](https://swagger.io/tools/swagger-ui/) or [Redoc](https://github.com/Redocly/redoc). You can either both interpret endpoints to OpenAPI's yaml and expose them in a single step (see above), or you can do that separately. 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 `tapir-swagger-ui`: ```scala "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "1.13.31" ``` or `tapir-redoc`: ```scala "com.softwaremill.sttp.tapir" %% "tapir-redoc" % "1.13.31" ``` Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: ```scala import sttp.apispec.openapi.circe.yaml.* import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import sttp.tapir.server.netty.{NettyFutureServerInterpreter, FutureRoute} import sttp.tapir.swagger.SwaggerUI import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global val myEndpoints: Seq[AnyEndpoint] = ??? val docsAsYaml: String = OpenAPIDocsInterpreter().toOpenAPI(myEndpoints, "My App", "1.0").toYaml // add to your netty routes val swaggerUIRoute: FutureRoute = NettyFutureServerInterpreter().toRoute(SwaggerUI[Future](docsAsYaml)) ``` ## 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, 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. * `defaultDecodeFailureOutput`: if an endpoint does not define a Bad Request response in `errorOut`, 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 returns `None`: ```scala OpenAPIDocsOptions.default.copy(defaultDecodeFailureOutput = _ => None) ``` * `markOptionsAsNullable`: by default, optional fields are not marked as `nullable` in the OpenAPI schema. If your codec allows `null` values, you can explicitly specify this in documentation by changing this option. * `schemaName`: specifies how schema names are created from the full type name. By default, this takes the last component of a dot-separated type name. Suffixes might be added at a later stage to disambiguate between different schemas with same names. * `failOnDuplicateOperationId`: if set to `true`, the interpreter will throw an exception if it encounters two endpoints with the same operation id. An OpenAPI document with duplicate operation ids is not valid. Code generators can silently drop duplicates. This is also verified by the [endpoint verifier](../testing.md). * `failOnDuplicateSchemaName`: if set to `true`, the interpreter will throw an exception if it encounters two schemas which (without automatic deduplication by adding a numeric suffix) would be identical. Having automatically resolved de-duplications might result in different names depending on the order of endpoints. This might result in false positive changes in the OpenApi document. ## 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](../endpoint/schemas.md) removing the name. ## Authentication inputs and security requirements Multiple non-optional authentication inputs indicate that all the given authentication values should be provided, that is they will form a single security requirement, with multiple schemes, e.g.: ```scala import sttp.model.headers.WWWAuthenticateChallenge import sttp.tapir.* val multiAuthEndpoint = endpoint.post .securityIn(auth.apiKey(header[String]("token"), WWWAuthenticateChallenge("ApiKey").realm("realm"))) .securityIn(auth.apiKey(header[String]("signature"), WWWAuthenticateChallenge("ApiKey").realm("realm"))) ``` A single optional authentication method can be described by mapping to optional types, e.g. `bearer[Option[String]]`. Hence, two security requirements will be created: an empty one, and one corresponding to the given authentication input. If there are multiple **optional** authentication methods, they will be treated as alternatives, and separate alternative security requirements will be created for them. However, this will not include the empty requirement, making authentication mandatory. If authentication should be optional, an empty security requirement will be added if an `emptyAuth` input is added (which doesn't map to any values in the request, but only serves as a marker). ```{note} Note that even though multiple optional authentication methods might be rendered as alternatives in the documentation, when running the server, you'll need to additionally check that at least one authentication input is provided. This can be done in the security logic, server logic, or by mapping the inputs using .mapDecode, as in the below example: ``` ```scala import sttp.model.headers.WWWAuthenticateChallenge import sttp.tapir.* val alternativeAuthEndpoint = endpoint.securityIn( // auth.apiKey(...).and(auth.apiKey(..)) will map the request headers to a tuple (Option[String], Option[String]) auth.apiKey(header[Option[String]]("token-old"), WWWAuthenticateChallenge("ApiKey").realm("realm")) .and(auth.apiKey(header[Option[String]]("token-new"), WWWAuthenticateChallenge("ApiKey").realm("realm"))) // mapping this tuple to an Either[String, String], and reporting a decode error if both values are missing .mapDecode { case (Some(oldToken), _) => DecodeResult.Value(Left(oldToken)) case (_, Some(newToken)) => DecodeResult.Value(Right(newToken)) case (None, None) => DecodeResult.Missing } { case Left(oldToken) => (Some(oldToken), None) case Right(newToken) => (None, Some(newToken)) } ) val alternativeOptionalAuthEndpoint = alternativeAuthEndpoint.securityIn(emptyAuth) ``` Finally, optional authentication inputs can be grouped into security requirements using `EndpointInput.Auth.group(String)`. Group names are arbitrary (and aren't rendered in the documentation), but they need to be the same for a single group. Groups should only be used on optional authentication inputs. All such inputs in a single group will become a single security requirement when rendered in OpenAPI. As before, the fact that values for all inputs in a group are provided, needs to be checked on the server-side, either through decoding or server logic. ## OpenAPI Specification Extensions It's possible to extend specification with [extensions](https://swagger.io/docs/specification/openapi-extensions/). Specification extensions can be added by first importing an extension method, and then calling the `docsExtension` method which manipulates the appropriate attribute on the schema, endpoint or endpoint input/output: ```scala import sttp.apispec.openapi.* import sttp.apispec.openapi.circe.* import sttp.apispec.openapi.circe.yaml.* import sttp.tapir.* import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* import io.circe.generic.auto.* import sttp.tapir.docs.apispec.DocsExtension import sttp.tapir.docs.apispec.DocsExtensionAttribute.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter case class FruitAmount(fruit: String, amount: Int) case class MyExtension(string: String, int: Int) implicit val fruitAmountSchemaWithMyExtension: Schema[FruitAmount] = Schema.derived[FruitAmount].docsExtension("hello", MyExtension("world", 42)) 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 a tool such as [quicklens](https://github.com/softwaremill/quicklens). If you are using `tapir-swagger-ui` you need to set `withShowExtensions` option for `SwaggerUIOptions`. ## Hiding inputs/outputs It's possible to hide an input/output from the OpenAPI description using following syntax: ```scala import sttp.tapir.* val acceptHeader: EndpointInput[String] = header[String]("Accept").schema(_.hidden(true)) ``` ## Using SwaggerUI with sbt-assembly The `tapir-swagger-ui` and `tapir-swagger-ui-bundle` 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](https://github.com/sbt/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: ```scala assemblyMergeStrategy in assembly := { case PathList("META-INF", "maven", "org.webjars", "swagger-ui", "pom.properties") => MergeStrategy.singleOrError case PathList("META-INF", "resources", "webjars", "swagger-ui", _*) => MergeStrategy.singleOrError case PathList("META-INF", _*) => MergeStrategy.discard // Optional, but usually required case x => val oldStrategy = (assemblyMergeStrategy in assembly).value oldStrategy(x) } ``` # Generating AsyncAPI documentation To use, add the following dependencies: ```scala "com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "1.13.31" "com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` Tapir contains a case class-based model of the asyncapi data structures in the `asyncapi/asyncapi-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 using the `sttp.tapir.docs.asyncapi.AsyncAPIInterpreter` object: ```scala import sttp.apispec.asyncapi.AsyncAPI import sttp.capabilities.pekko.PekkoStreams import sttp.tapir.* import sttp.tapir.docs.asyncapi.AsyncAPIInterpreter import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* case class Response(msg: String, count: Int) val echoWS = endpoint.out( webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](PekkoStreams)) val docs: AsyncAPI = AsyncAPIInterpreter().toAsyncAPI(echoWS, "Echo web socket", "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 `AsyncAPI` one can be made easier by using a lens library, e.g. [Quicklens](https://github.com/adamw/quicklens). The documentation is generated in a large part basing on [schemas](../endpoint/codecs.md#schemas). Schemas can be [automatically derived and customised](../endpoint/schemas.md). Quite often, you'll need to define the servers, through which the API can be reached. Any servers provided to the `.toAsyncAPI` invocation will be supplemented with security requirements, as specified by the endpoints: ```scala import sttp.apispec.asyncapi.Server val docsWithServers: AsyncAPI = AsyncAPIInterpreter().toAsyncAPI( echoWS, "Echo web socket", "1.0", List("production" -> Server("api.example.com", "wss")) ) ``` Servers can also be later added through methods on the `AsyncAPI` object. Multiple endpoints can be converted to an `AsyncAPI` instance by calling the method using a list of endpoints. The asyncapi case classes can then be serialised, either to JSON or YAML using [Circe](https://circe.github.io/circe/): ```scala import sttp.apispec.asyncapi.circe.yaml.* println(docs.toYaml) ``` ## Options Options can be customised by providing an instance of `AsyncAPIDocsOptions` to the interpreter: * `subscribeOperationId`: basing on the endpoint's path and the entire endpoint, determines the id of the subscribe operation. This can be later used by code generators as the name of the method to receive messages from the socket. * `publishOperationId`: as above, but for publishing (sending messages to the web socket). ## 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](../endpoint/schemas.md) removing the name. ## AsyncAPI Specification Extensions AsyncAPI supports adding [extensions](https://www.asyncapi.com/docs/specifications/2.0.0#specificationExtensions) similarly as in OpenAPI. Specification extensions can be added by first importing an extension method, and then calling the `docsExtension` method which manipulates the appropriate attribute on the schema, endpoint or endpoint input/output: ```scala import sttp.tapir.docs.apispec.DocsExtensionAttribute.* endpoint .post .in(query[String]("hi").docsExtension("x-query", 33)) .docsExtension("x-endpoint-level-string", "world") ``` There are `requestsDocsExtension` and `responsesDocsExtension` methods to add extensions to a `websocketBody`. Take a look at **OpenAPI Specification Extensions** section of [documentation](../docs/openapi.md) to get a feeling on how to use it. ## Exposing AsyncAPI documentation AsyncAPI documentation can be exposed through the [AsyncAPI playground](https://playground.asyncapi.io). # Generating JSON Schema You can conveniently generate JSON schema from Tapir schema, which can be derived from your Scala types. Use `TapirSchemaToJsonSchema`: ```scala "com.softwaremill.sttp.tapir" %% "tapir-apispec-docs" % "1.13.31" ``` Schema generation can now be performed like in the following example: ```scala import sttp.apispec.{Schema => ASchema} import sttp.tapir.* import sttp.tapir.docs.apispec.schema.* import sttp.tapir.generic.auto.* object Childhood { case class Child(age: Int, height: Option[Int]) } case class Parent(innerChildField: Child, childDetails: Childhood.Child) case class Child(childName: String) // to illustrate unique name generation val tSchema = implicitly[Schema[Parent]] val jsonSchema: ASchema = TapirSchemaToJsonSchema( tSchema, markOptionsAsNullable = true, metaSchema = MetaSchemaDraft04 // default // schemaName = sttp.atpir.docs.apispec.defaultSchemaName // default ) ``` All the nested schemas will be referenced from the `$defs` element. ## Serializing JSON Schema In order to generate a JSON representation of the schema, you can use Circe. For example, with sttp [jsonschema-circe](https://github.com/softwaremill/sttp-apispec) module: ```scala "com.softwaremill.sttp.apispec" %% "jsonschema-circe" % "..." ``` you will get a codec for `sttp.apispec.Schema`: ```scala import io.circe.Printer import io.circe.syntax.* import sttp.apispec.circe.* import sttp.apispec.{Schema => ASchema} import sttp.tapir.* import sttp.tapir.docs.apispec.schema.* import sttp.tapir.generic.auto.* import sttp.tapir.Schema.annotations.title object Childhood { @title("my child") case class Child(age: Int, height: Option[Int]) } case class Parent(innerChildField: Child, childDetails: Childhood.Child) case class Child(childName: String) val tSchema = implicitly[Schema[Parent]] val jsonSchema: ASchema = TapirSchemaToJsonSchema( tSchema, markOptionsAsNullable = true) // JSON serialization val schemaAsJson = jsonSchema.asJson val schemaStr: String = Printer.spaces2.print(schemaAsJson.deepDropNullValues) ``` The title annotation of the object will be by default the name of the case class. You can customize it with `@title` annotation. You can also disable generation of default title fields by setting an option `addTitleToDefs` to `false`. This example will produce following String: ```json { "$schema" : "http://json-schema.org/draft-04/schema#", "required" : [ "innerChildField", "childDetails" ], "type" : "object", "properties" : { "innerChildField" : { "$ref" : "#/$defs/Child" }, "childDetails" : { "$ref" : "#/$defs/Child1" } }, "$defs" : { "Child" : { "title" : "Child", "required" : [ "childName" ], "type" : "object", "properties" : { "childName" : { "type" : "string" } } }, "Child1" : { "title" : "my child", "required" : [ "age" ], "type" : "object", "properties" : { "age" : { "type" : "integer", "format" : "int32" }, "height" : { "type" : [ "integer", "null" ], "format" : "int32" } } } } } ``` # Testing ## Server endpoints If you are exposing endpoints using one of the server interpreters, you might want to test a complete server endpoint, how validations, built-in and custom [interceptors](server/interceptors.md) and [error handling](server/errors.md) behaves. This might be done while providing alternate, or using the original [server logic](server/logic.md). Such testing is possible by creating a special [sttp client](https://sttp.softwaremill.com) backend. When a request is sent using such a backend, no network traffic is happening. Instead, the request is decoded using the provided endpoints, the appropriate logic is run, and then the response is encoded - as in a real server interpreter. But similar as with a request, the response isn't sent over the network, but returned directly to the caller. Hence, binding to an interface isn't necessary to run these tests. You can define the sttp requests by hand, to see how an arbitrary request will be handled by your server endpoints. Or, you can interpret an endpoint as a [client](client/sttp.md), to test both how the client & server interpreters interact with your endpoints. The special backend that is described above is based on a `BackendStub`, which can be used to stub arbitrary behaviors. See the [sttp documentation](https://sttp.softwaremill.com/en/latest/testing/stub.html) for details. ```{note} The following example code uses sttp-client v4. The same functionality is available using sttp-client v3, but using a different dependency and import (from the `sttp.tapir.server.stub` package). Moreover, some names might differ, e.g. `SttpBackendStub` instead of `BackendStub`. ``` Tapir builds upon the `BackendStub` to enable stubbing using `Endpoint`s or `ServerEndpoint`s. To start, add the dependency: ```scala // used below, sttp-client v4 "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub4-server" % "1.13.31" // for sttp-client v3 "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.13.31" ``` Let's assume you are using the [pekko http](server/pekkohttp.md) interpreter. Given the following server endpoint: ```scala import sttp.tapir.* import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future val someEndpoint: Endpoint[String, Unit, String, String, Any] = endpoint.get .in("api") .securityIn(auth.bearer[String]()) .out(stringBody) .errorOut(stringBody) val someServerEndpoint: ServerEndpoint[Any, Future] = someEndpoint .serverSecurityLogic(token => Future.successful { if (token == "password") Right("user123") else Left("unauthorized") } ) .serverLogic(user => _ => Future.successful(Right(s"hello $user"))) ``` A test which verifies how this endpoint behaves when interpreter as a server might look as follows: ```scala import org.scalatest.flatspec.AsyncFlatSpec import org.scalatest.matchers.should.Matchers import sttp.client4.* import sttp.client4.testing.BackendStub import sttp.tapir.server.stub4.TapirStubInterpreter class MySpec extends AsyncFlatSpec with Matchers: it should "work" in { // given val backendStub: Backend[Future] = TapirStubInterpreter(BackendStub.asynchronousFuture) .whenServerEndpoint(someServerEndpoint) .thenRunLogic() .backend() // when val response = basicRequest .get(uri"http://test.com/api/users/greet") .header("Authorization", "Bearer password") .send(backendStub) // then response.map(_.body shouldBe Right("hello user123")) } ``` The `.backend` method creates the enriched `BackendStub`, using the provided server endpoints and their behaviors. Any requests will be handled by a stub server interpreter, using the complete request handling logic. Projects generated using [adopt-tapir](https://adopt-tapir.softwaremill.com) include a test which uses the above approach. ### Custom interceptors Custom interceptors can be provided to the stub. For example, to test custom exception handling, we might have the following customized pekko http options: ```scala import sttp.tapir.server.interceptor.exception.ExceptionHandler import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions import sttp.tapir.server.model.ValuedEndpointOutput import sttp.model.StatusCode val exceptionHandler = ExceptionHandler.pure[Future](ctx => Some(ValuedEndpointOutput( stringBody.and(statusCode), (s"failed due to ${ctx.e.getMessage}", StatusCode.InternalServerError) )) ) val customOptions: CustomiseInterceptors[Future, PekkoHttpServerOptions] = { import scala.concurrent.ExecutionContext.Implicits.global PekkoHttpServerOptions.customiseInterceptors .exceptionHandler(exceptionHandler) } ``` Testing such an interceptor requires simulating an exception being thrown in the server logic: ```scala class MySpec2 extends AsyncFlatSpec with Matchers: it should "use my custom exception handler" in { // given val stub = TapirStubInterpreter(customOptions, BackendStub.asynchronousFuture) .whenEndpoint(someEndpoint) .thenThrowException(new RuntimeException("error")) .backend() // when basicRequest .get(uri"http://test.com/api") .send(stub) // then .map(_.body shouldBe Left("failed due to error")) } ``` Note that to provide alternate success/error outputs given a `ServerEndpoint`, the endpoint will have to be typed using the full type information, that is using the `ServerEndpoint.Full` alias. ### Limitations Ranged file responses (a `FileRange` with a non-empty range) are not supported on Scala.js, as materializing the partial body requires file-system access; the stub throws an `UnsupportedOperationException`. ## External APIs If you are integrating with an external API, which is described using tapir's `Endpoint`s, or if you'd like to create an [sttp client stub backend](https://sttp.softwaremill.com/en/latest/testing/stub.html), with arbitrary behavior for requests matching an endpoint, you can use the tapir `SttpBackendStub` extension methods. Similarly as when testing server interpreters, add the dependency: ```scala // used below, sttp-client v4 "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub4-server" % "1.13.31" // for sttp-client v3 "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.13.31" ``` And the following imports: ```scala import sttp.client4.testing.{BackendStub, SyncBackendStub} import sttp.tapir.server.stub4.* ``` Then, given the following endpoint: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* case class ResponseWrapper(value: Double) val e = endpoint .in("api" / "sometest4") .in(query[Int]("amount")) .post .out(jsonBody[ResponseWrapper]) ``` Any endpoint can be converted to `BackendStub`: ```scala val backend: SyncBackendStub = BackendStub .synchronous .whenRequestMatchesEndpoint(e) .thenSuccess(ResponseWrapper(1.0)) ``` ## Black box testing When testing an application as a whole component, running for example in docker, you might want to stub external services with which your application interacts. To do that you might want to use well-known solutions like e.g. [wiremock](http://wiremock.org/) or [mock-server](https://www.mock-server.com/), but if their api is described using tapir you might want to use [livestub](https://github.com/softwaremill/livestub), which combines nicely with the rest of the sttp ecosystem. ### Black box testing with mock-server integration If you are writing integration tests for your application which communicates with some external systems (e.g payment providers, SMS providers, etc.), you could stub them using tapir's integration with [mock-server](https://www.mock-server.com/) Add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "sttp-mock-server" % "1.13.31" ``` Imports: ```scala import sttp.tapir.server.mockserver.* ``` Then, given the following endpoint: ```scala import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* case class SampleIn(name: String, age: Int) case class SampleOut(greeting: String) val sampleJsonEndpoint = endpoint.post .in("api" / "v1" / "json") .in(header[String]("X-RequestId")) .in(jsonBody[SampleIn]) .errorOut(stringBody) .out(jsonBody[SampleOut]) ``` and having any `SttpBackend` instance (for example, `TryHttpURLConnectionBackend` or with any other, arbitrary effect `F[_]` type), convert any endpoint to a **mock-server** expectation: ```scala import sttp.client4.* import sttp.client4.httpclient.HttpClientSyncBackend import sttp.client4.wrappers.TryBackend val testingBackend = TryBackend(HttpClientSyncBackend()) val mockServerClient = SttpMockServerClient(baseUri = uri"http://localhost:1080", testingBackend) val in = "request-id-123" -> SampleIn("John", 23) val out = SampleOut("Hello, John!") val expectation = mockServerClient .whenInputMatches(sampleJsonEndpoint)((), in) .thenSuccess(out) .get ``` Then you can try to send requests to the mock-server as you would do with live integration: ```scala import sttp.tapir.client.sttp4.SttpClientInterpreter import sttp.client4.* import sttp.client4.httpclient.HttpClientSyncBackend import sttp.client4.wrappers.TryBackend val testingBackend = TryBackend(HttpClientSyncBackend()) val in = "request-id-123" -> SampleIn("John", 23) val out = SampleOut("Hello, John!") val result = SttpClientInterpreter() .toRequest(sampleJsonEndpoint, baseUri = Some(uri"http://localhost:1080")) .apply(in) .send(testingBackend) .get result == out ``` ## Endpoints verification To use, add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-testing" % "1.13.31" ``` ### Shadowed endpoints It is possible to define a list of endpoints where some endpoints will be overlapping with each other. In such case when all matching requests will be handled by the first endpoint; the second endpoint will always be omitted. To detect such cases one can use `EndpointVerifier` util class which takes an input of type `List[AnyEndpoint]` an outputs `Set[EndpointVerificationError]`. Example 1: ```scala import sttp.tapir.testing.EndpointVerifier val e1 = endpoint.get.in("x" / paths) val e2 = endpoint.get.in("x" / "y" / "x") val e3 = endpoint.get.in("x") val e4 = endpoint.get.in("y" / "x") val res = EndpointVerifier(List(e1, e2, e3, e4)) ``` Results in: ```scala res.toString // res2: String = "Set(GET /x/y/x, is shadowed by: GET /x/*, GET /x, is shadowed by: GET /x/*)" ``` Example 2: ```scala import sttp.tapir.testing.EndpointVerifier val e1 = endpoint.get.in(path[String].name("y_1") / path[String].name("y_2")) val e2 = endpoint.get.in(path[String].name("y_3") / path[String].name("y_4")) val res = EndpointVerifier(List(e1, e2)) ``` Results in: ```scala res.toString // res3: String = "Set(GET /{y_3}/{y_4}, is shadowed by: GET /{y_1}/{y_2})" ``` Note that the above takes into account only the method & the shape of the path. It does *not* take into account possible decoding failures: these might impact request-endpoint matching, and the exact behavior is determined by the [`DecodeFailureHandler`](server/errors.md#decode-failures) used. ### Incorrect path at endpoint It is possible to define an endpoint where some part of an input will consume whole remaining path. That case can lead to situation where all other inputs defined after `paths` wildcard segment are omitted. To detect such cases one can use `EndpointVerifier` util class which takes an input of type `List[AnyEndpoint]` an outputs `Set[EndpointVerificationError]`. Example 1: ```scala import sttp.tapir.testing.EndpointVerifier val e = endpoint.options.in("a" / "b" / "c").securityIn("x" / "y" / paths) val result = EndpointVerifier(List(e)) ``` Results in: ```scala result.toString // res4: String = "Set(A wildcard pattern in OPTIONS /x/y/*/a/b/c shadows the rest of the paths at index 2)" ``` ### Duplicated method definitions at endpoint It is possible to define an endpoint where there are methods multiple times defined. To detect such cases one can use `EndpointVerifier` util class which takes an input of type `List[AnyEndpoint]` an outputs `Set[EndpointVerificationError]`. Example 1: ```scala import sttp.tapir.testing.EndpointVerifier val ep = endpoint.options.in("a" / "b" / "c").get val result2 = EndpointVerifier(List(ep)) ``` Results in: ```scala result2.toString // res5: String = "Set(An endpoint OPTIONS GET /a /b /c -> -/- have multiple method definitions: List(OPTIONS, GET))" ``` ### Duplicated endpoint names Duplicate endpoint names will generate duplicate operation ids, when generating OpenAPI or AsyncAPI documentation. As the operation ids should be unique, this is reported as an error: Example 1: ```scala import sttp.tapir.testing.EndpointVerifier val ep1 = endpoint.name("e1").get.in("a") val ep2 = endpoint.name("e1").get.in("b") val result3 = EndpointVerifier(List(ep1, ep2)) ``` Results in: ```scala result3.toString // res6: String = "Set(Duplicate endpoints names found: e1)" ``` ## OpenAPI schema compatibility The `OpenAPIVerifier` provides utilities for verifying that client and server endpoints are consistent with an OpenAPI specification. This ensures that endpoints defined in your code correspond to those documented in the OpenAPI schema, and vice versa. To use the `OpenAPIVerifier`, add the following dependency: ```scala "com.softwaremill.sttp.tapir" %% "tapir-openapi-verifier" % "1.13.31" ``` The `OpenAPIVerifier` supports two key verification scenarios: 1. **Server Verification**: Ensures that all endpoints defined in the OpenAPI specification are implemented by the server. 2. **Client Verification**: Ensures that the client implementation matches the OpenAPI specification. As a result, you get a list of issues that describe the incomapatibilities, or an empty list, if the endpoints and schema are compatible. ### Example Usage #### Server Endpoint Verification ```scala import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIVerifier import sttp.tapir.json.circe.* val clientOpenAPISpecification: String = """ openapi: 3.0.0 info: title: Sample API version: 1.0.0 paths: /users: get: summary: Get users responses: "200": description: A list of users content: application/json: schema: type: array items: type: string """ val serverEndpoints = List( endpoint.get.in("users").out(jsonBody[List[String]]) ) val serverIssues = OpenAPIVerifier.verifyServer(serverEndpoints, clientOpenAPISpecification) ``` #### Client Endpoint Verification ```scala import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIVerifier import sttp.tapir.json.circe.* val serverOpenAPISpecification: String = """ openapi: 3.0.0 info: title: Sample API version: 1.0.0 paths: /users: get: summary: Get users responses: "200": description: A list of users content: application/json: schema: type: array items: type: string """.stripMargin val clientEndpoints = List( endpoint.get.in("users").out(jsonBody[List[String]]) ) val clientIssues = OpenAPIVerifier.verifyClient(clientEndpoints, serverOpenAPISpecification) ``` # Generate endpoint definitions from an OpenAPI YAML ```{note} This is a relatively mature implementation that should be sufficiently capable for the majority of use-cases, but nonetheless does not yet completely cover the openapi spec. Pull requests or issues for missing or incorrectly-implemented functionality are highly encouraged. ``` ## Installation steps Add the sbt plugin to the `project/plugins.sbt`: ```scala addSbtPlugin("com.softwaremill.sttp.tapir" % "sbt-openapi-codegen" % "1.13.31") ``` Enable the plugin for your project in the `build.sbt`: ```scala enablePlugins(OpenapiCodegenPlugin) ``` Add your OpenApi file to the project, and override the `openapiSwaggerFile` setting in the `build.sbt`: ```scala openapiSwaggerFile := baseDirectory.value / "swagger.yaml" ``` At this point your compile step will try to generate the endpoint definitions to the `sttp.tapir.generated.TapirGeneratedEndpoints` object, where you can access the defined case-classes and endpoint definitions. ## Usage and options The generator currently supports these settings, you can override them in the `build.sbt`; ```{eval-rst} ===================================== ==================================== ================================================================================================== setting default value description ===================================== ==================================== ================================================================================================== openapiSwaggerFile baseDirectory.value / "swagger.yaml" The swagger file with the api definitions. openapiPackage sttp.tapir.generated The name for the generated package. openapiObject TapirGeneratedEndpoints The name for the generated object. openapiUseHeadTagForObjectName false If true, put endpoints in separate files based on first declared tag. openapiJsonSerdeLib circe The json serde library to use. openapiXmlSerdeLib cats-xml The xml serde library to use. openapiValidateNonDiscriminatedOneOfs true Whether to fail if variants of a oneOf without a discriminator cannot be disambiguated. openapiMaxSchemasPerFile 400 Maximum number of schemas to generate in a single file (tweak if hitting javac class size limits). openapiAdditionalPackages Nil Additional packageName/swaggerFile pairs for generating from multiple schemas openapiStreamingImplementation fs2 Implementation for streamTextBody. Supports: akka, fs2, pekko, zio. fs2 defaults to using the IO effect -- an alternative effect type can be specified with fs2-my.fully.qualified.Effect openapiGenerateEndpointTypes false Whether to emit explicit types for endpoint defns openapiDisableValidatorGeneration false If true, we will not generate validation for constraints (min, max, pattern etc) openapiUseCustomJsoniterSerdes false If true and openapiJsonSerdeLib = jsoniter, serdes will be generated to use custom 'openapi' make defns. May help with flaky compilation, but requires jsoniter-scala >= 2.36.0+ openapiPackageDependencies Map.empty[String, String] Allows the generated code for a key package to 'depend on' the generated package in the value. This will deduplicate repeated schema declarations between the openapis, with the generated code for the 'key' package defining type (and sometimes val) aliases to the duplicates in the 'value' package. This is still experimental - significantly, the type is likely to change to a Map[String, Seq[String]] in the near future to permit multiple 'inheritance', and there may be bugs in the implementation. openapiSeperateFilesForModels false When true, models will be written to individual files under $pkg.models, with type aliases and helpers living under `package.scala` in a package object openapiAlwaysGenerateParamSupport false When true, all enums will be generated with param & json support, even if not used in those positions. This is useful for definitions that will be reused with `openapiPackageDependencies` openapiAddDisambiguationCodes false When true, if multiple status codes in same group (i.e. all error, or all success) return the same schema, they will be paired with a status code object as (T, StatusCode). 'Default' codes will map to an additional StatusCode output. When false, the type will remain a T and default codes will be treated as 400s. ===================================== ==================================== ================================================================================================== ``` The general usage is; ```scala import sttp.apispec.openapi.circe.yaml.* import sttp.tapir.generated.* import sttp.tapir.docs.openapi.* val docs = TapirGeneratedEndpoints.generatedEndpoints.toOpenAPI("My Bookshop", "1.0") ``` ### Support specification extensions Generator behaviour can be further configured by specifications on the input openapi spec. Example: ```yaml paths: x-tapir-codegen-security-path-prefixes: - '/security-group/{securityGroupName}' # any path prefixes matching this pattern will be considered 'securityIn' '/my-endpoint': post: x-tapir-codegen-directives: [ 'json-body-as-string' ] # This will customise what the codegen generates for this endpoint requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/MyModel' responses: "204": description: "No response" ``` Supported specifications are: - x-tapir-codegen-security-path-prefixes: supported on the paths object. This is an array of strings representing path prefixes. The longest matching prefix of each path will be treated as a security input, rather than as a standard 'in' value. - x-tapir-codegen-directives: supported on openapi operations. This is an array of string flags. Supported values are: ```{eval-rst} ========================= =================================================================================================================================== name description ========================= =================================================================================================================================== json-body-as-string If present on an operation, all application/json requests and responses will be interpreted mapped to a string with stringJsonBody force-eager If present on an operation, all content types will be forced to eager, even if the default implementation is streaming force-streaming If present on an operation, all content types will be forced to streaming, even if the default implementation is eager, unless it is in error position (which is always eager) force-req-body-eager Like force-eager, but applies only to req body force-resp-body-eager Like force-eager, but applies only to resp body force-req-body-streaming Like force-streaming, but applies only to req body force-resp-body-streaming Like force-streaming, but applies only to resp body ========================= =================================================================================================================================== ``` ### Output files To expand on the `openapiUseHeadTagForObjectName` setting a little more, suppose we have the following endpoints: ```yaml paths: /foo: get: tags: - Baz - Foo put: tags: [ ] /bar: get: tags: - Baz - Bar ``` In this case 'head' tag for `GET /foo` and `GET /bar` would be 'Baz', and `PUT /foo` has no tags (and thus no 'head' tag). If `openapiUseHeadTagForObjectName = false` (assuming default settings for the other flags) then all endpoint definitions will be output to the `TapirGeneratedEndpoints.scala` file, which will contain a single `object TapirGeneratedEndpoints`. If `openapiUseHeadTagForObjectName = true`, then the `GET /foo` and `GET /bar` endpoints would be output to a `Baz.scala` file, containing a single `object Baz` with those endpoint definitions; the `PUT /foo` endpoint, by dint of having no tags, would be output to the `TapirGeneratedEndpoints` file, along with any schema and parameter definitions. Files can be generated from multiple openapi schemas if `openapiAdditionalPackages` is configured; for example: ```scala openapiAdditionalPackages := List( "sttp.tapir.generated.v1" -> baseDirectory.value / "src" / "main" / "resources" / "openapi_v1.yml") ``` would generate files in the package `sttp.tapir.generated.v1` based on the `openapi_v1.yml` schema at the provided location. This would be in addition to files generated in `openapiPackage` from the specs configured by `openapiSwaggerFile` ### Json Support ```{eval-rst} ===================== ================================================================== =================================================================== openapiJsonSerdeLib required dependencies Conditional requirements ===================== ================================================================== =================================================================== circe "io.circe" %% "circe-core" "com.beachape" %% "enumeratum-circe" (scala 2 enum support). "io.circe" %% "circe-generic" "org.latestbit" %% "circe-tagged-adt-codec" (scala 3 enum support). jsoniter "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-circe" (free-form json support) "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" ===================== ================================================================== =================================================================== ``` ### XML Support Xml support is still fairly experimental. Available options are 'cats-xml' and 'none'. 'none' will fallback to a streaming binary 'non-implementation'. The minimal supported version of cats-xml is 0.0.20 for scala 2 and TBD for scala 3. ```{eval-rst} ===================== ======================================================================================== openapiXmlSerdeLib required dependencies ===================== ======================================================================================== cats-xml "com.github.geirolz" %% "cats-xml" "com.github.geirolz" %% "cats-xml-generic" none ===================== ======================================================================================== ``` ### Limitations Currently, string-like enums in Scala 2 depend upon the enumeratum library (`"com.beachape" %% "enumeratum"`). For Scala 3 we derive native enums, and depend on `"io.github.bishabosha" %% "enum-extensions"` for generating query param serdes. Models containing binary data cannot be re-used between json and multi-part form endpoints, due to having different representation types for the binary data We currently miss a few OpenApi features. Notable are: - anyOf - not all validation is supported (readOnly/writeOnly, and minProperties/maxProperties on heterogeneous object schemas, are currently unsupported) - some model types are not yet supported (e.g. `decimal`) # Stability of modules The modules are categorised using the following levels: * **stable**: binary compatibility is guaranteed within a major version; adheres to semantic versioning * **stabilising**: the API is mostly stable, with rare binary-incompatible changes possible in minor releases (only if necessary) * **experimental**: API can change significantly even in patch releases The major version is increased when there are binary-incompatible changes in **stable** modules. The minor version is increased when there are significant new features in **stable** modules (keeping compatibility), or binary-incompatible changes in **stabilising** modules. The patch version is increased when there are binary-compatible changes in **stable** / **stabilising** modules, any changes in **exeperimental** modules, or when a new module is added (e.g. a new integration). ## Main modules | Module | Level | |----------------|-------------| | core (Scala 2) | stable | | core (Scala 3) | stabilising | | server-core | stabilising | | client-core | stabilising | | files | stabilising | ## Server interpreters | Module | Level | |-----------|--------------| | akka-http | stabilising | | armeria | stabilising | | finatra | stabilising | | http4s | stabilising | | netty | stabilising | | nima | experimental | | pekko-http| stabilising | | play | stabilising | | vertx | stabilising | | zio-http | stabilising | ## Client interpreters | Module | Level | |--------|-------------| | sttp | stabilising | | play | stabilising | | http4s | stabilising | ## Documentation interpreters | Module | Level | |----------|-------------| | openapi | stabilising | | asyncapi | stabilising | ## Serverless interpreters | Module | Level | |---------------|--------------| | aws-lambda | experimental | | aws-sam | experimental | | aws-terraform | experimental | ## Integration modules | Module | Level | |---------------|--------------| | cats | stabilising | | cats-effect | stabilising | | derevo | stabilising | | enumeratum | stabilising | | newtype | stabilising | | monix-newtype | stabilising | | refined | stabilising | | zio | stabilising | | zio-prelude | experimental | | iron | experimental | ## JSON modules | Module | Level | |------------|--------------| | circe | stabilising | | json4s | stabilising | | jsoniter | stabilising | | play-json | stabilising | | spray-json | stabilising | | tethys | stabilising | | upickle | stabilising | | pickler | experimental | | zio-json | experimental | ## Testing modules | Module | Level | |-----------|--------------| | testing | stabilising | | sttp-mock | experimental | | sttp-stub | stabilising | ## Observability modules | Module | Level | |-----------------------|-------------| | opentelemetry-metrics | stabilising | | prometheus-metrics | stabilising | ## Other modules | Module | Level | |--------------------|--------------| | openapi-codegen | experimental | # Architecture Decision Records ADRs covering some of Tapir's design decisions are available in the `adr` directory of the repository. They are [available here](https://github.com/softwaremill/tapir/tree/master/doc/adr). # Contributing All suggestions welcome :)! If you'd like to contribute, see the list of [issues](https://github.com/softwaremill/tapir/issues) and pick one! Or report your own. If you have an idea you'd like to discuss, that's always a good option. If you are having doubts on the _why_ or _how_ something works, don't hesitate to ask a question on [discourse](https://softwaremill.community/c/tapir) or via github. This probably means that the documentation, scaladocs or code is unclear and can be improved for the benefit of all. ## Conventions ### Enumerations Scala 3 introduces `enum`, which can be used to represent sealed hierarchies with simpler syntax, or actual "true" enumerations, that is parameterless enums or sealed traits with only case objects as children. Tapir needs to treat the latter differently, in order to allow using OpenAPI `enum` elements and derive JSON codecs which represent them as simple values (without discriminator). Let's use the name `enumeration` in Tapir codebase to represent these "true" enumerations and avoid ambiguity. ## JDK version To ensure that Tapir can be used in a wide range of projects, the CI job uses JDK11 for most of the modules. There are exceptions (like `netty-server-sync` and `nima-server`) which require JDK version >= 21. This requirement is adressed by the build matrix in `.github/workflows/ci.yml`, which runs separate builds on a newer Java version, and sets a `ONLY_LOOM` env variable, used by build.sbt to recognise that it should limit the scope of an aggregated task to these projects only. For local development, feel free to use any JDK >= 11. You can be on JDK 21, then with missing `ONLY_LOOM` variable you can still run sbt tasks on projects excluded from aggegate build, for example: ```scala nimaServer/Test/test nettyServerSync3/compile // etc. ``` ## Acknowledgments Tuple-concatenating code is copied from [akka-http](https://github.com/akka/akka-http/blob/master/akka-http/src/main/scala/akka/http/scaladsl/server/util/TupleOps.scala) Parts of generic derivation configuration is copied from [circe](https://github.com/circe/circe/blob/master/modules/generic-extras/src/main/scala/io/circe/generic/extras/Configuration.scala) Implementation of mirror for union and intersection types are originally implemented by [Iltotore](https://github.com/Iltotore) in [this gist](https://gist.github.com/Iltotore/eece20188d383f7aee16a0b89eeb887f) Tapir logo & stickers have been drawn by [impurepics](https://twitter.com/impurepics). # Goals of the project * programmer-friendly, human-comprehensible types, that you are not afraid to write down * (also inferencable by IntelliJ) * discoverable API through standard auto-complete * separate "business logic" from endpoint definition & documentation * as simple as possible to generate a server, client & docs * based purely on case class-based, immutable and reusable data structures * first-class OpenAPI support. Provide as much or as little detail as needed. * reasonably type safe: only, and as much types to safely generate the server/client/docs ## Similar projects There's a number of similar projects from which tapir draws inspiration: * [endpoints](https://github.com/julienrf/endpoints) * [typedapi](https://github.com/pheymann/typedapi) * [rho](https://github.com/http4s/rho) * [typed-schema](https://github.com/TinkoffCreditSystems/typed-schema) * [guardrail](https://github.com/twilio/guardrail) # gRPC Experimental feature - it's not complete and API may change tapir supports currently defining and exposing gRPC endpoints. There is also a handy tool for generating proto files from these endpoints' definitions. ## Modules * `protobuf` - contains a Protobuf protocol model and utilities that make generating proto files handy. This module should be used for generating proto files from endpoints definitions * `pbDirectProtobuf` - integration with `PBDirect` library. Should be used for auto codecs derivation. Currently, it's necessary to add this module for generating proto files with the `protobuf` module. * `akkaGrpcServer` - a module that provides `AkkaGrpcServerInterpreter` implementation. It should be used to serve tapir grpc endpoints. * `pekkoGrpcServer` - a module that provides `PekkoGrpcServerInterpreter` implementation. It can be used as an alternative to `akkaGrpcServer`. * `grpcExamples` - contains example use cases ## Defining endpoints Every gRPC endpoint requires specifying the following values: service name, method name, input format, output format. In tapir, we can simply pass service and method names as strings separated with `/` to the tapir endpoint input definition e.g. `endpoint.in("Library" / "AddBook")` where `"Library"` is a service name and `"AddBook"` is a method name. Definition of an endpoint's inputs and output format is very similar to the one that we use for JSONs. We can use an endpoint body constructor helper `sttp.tapir.grpc.protobuf.pbdirect.grpcBody[T]` that based on the type `T` create a body definition that can be passed to as input or output of a given endpoint e.g. `endpoint.in(grpcBody[AddSimpleBook]).out(grpcBody[SimpleBook])`. Mapping for basic types are defined (e.g. `java.lang.String` -> `string`), but the target protobuf type can be simply customized via `.attribute` schema feature (e.g. `implicit newSchema = implicitly[Derived[Schema[SimpleBook]]].value.modify(_.title)(_.attribute(ProtobufAttributes.ScalarValueAttribute, ProtobufScalarType.ProtobufBytes))` Currently, the only supported protocol is protobuf. On the server side, we use a [PBDirect](https://github.com/47degrees/pbdirect) library for encoding and decoding messages. It derives codecs from a class definition which means we do not depend on the generated code from a proto file. The `grpcBody` function takes as an implicit param tapir schema definition, that is used to generate a proto file, and PBDirect codecs for a given type. Currently, we don't support passing any metadata. ## Generating proto file With defined endpoints, we can move towards generating a proto file that we could use to generate clients for our API. The easiest way to do it is by using a built-in `ProtoSchemaGenerator`. We need to only choose a path for the file, package name, pass endpoints definitions, and finally invoke the `renderToFile` function e.g. ``` ProtoSchemaGenerator.renderToFile( path = "grpc/examples/src/main/protobuf/main.proto", packageName = "sttp.tapir.grpc.examples.gen", endpoints = Endpoints.endpoints ) ``` ## gRPC server With the proto file, we can generate clients for our API or the server-side code using [ScalaPb](https://scalapb.github.io) library. It's possible to connect the generated server code to tapir endpoints definitions, but it's not convenient since depends on auto-generated code. We strongly recommend using the new dedicated server interpreter `AkkaGrpcServerInterpreter`. It's built on top of `AkkaHttpServerInterpreter` and provides support for encoding and decoding HTTP2 binary messages. [Here](https://github.com/softwaremill/tapir/blob/master/grpc/examples/src/main/scala/sttp/tapir/grpc/examples/GrpcSimpleBooksExample.scala) you can find a simple example. It's worth mentioning that by adjusting slightly encoders/decoders it's possible to expose gRPC endpoints with `AkkaHttpServerInterpreter` as simple HTTP endpoints. This approach is not recommended, because it does not support transmitting multiple messages in a single http request. ## Supported data formats * Basic scalar types * Collections (repeated values) * Top level and nested products * Tapir schema derivation for coproducts (sealed traits) is supported, but we're missing codecs on the pbdirect side out of the box (oneof) # Migrating ## From 1.10.4 to 1.10.5 - `tapir-server-netty-loom` has been renamed to `tapir-netty-server-sync`, and is availavble only for Scala 3. Use imports from `sttp.tapir.server.netty.sync`, and start your server using `NettySyncServer()`. See [examples/HelloWorldNettySyncServer.scala](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettySyncServer.scala) for a full example. ## From 1.9.3 to 1.9.4 - `NettyConfig.defaultNoStreaming` has been removed, use `NettyConfig.default`. ## From 1.4 to 1.5 - `badRequestOnPathErrorIfPathShapeMatches` and `badRequestOnPathInvalidIfPathShapeMatches` have been removed from `DefaultDecodeFailureHandler`. These flags were causing confusion and incosistencies caused by specifics of ZIO and Play backends. Before tapir 1.5, keeping defaults (`false` and `true` respectively for these flags) meant that some path segment decoding failures (specifically, errors - when an exception has been thrown during decoding, but not for e.g. enumeration mismatches) were translated to a "no-match", meaning that the next endpoint was attempted. From 1.5, tapir defaults to a 400 Bad Request response to be sent instead, on all path decoding failures. - If your code sets `badRequestOnPathErrorIfPathShapeMatches = true` to override the default `false`, you can just remove this in tapir 1.5, it is the new default. - Similarly, if your code sets `.badRequestOnDecodeFailure` on endpoint path input, just remove this attribute. - If your code doesn't change this parameter and you update tapir, you should expect shape-matched path decoding failures to always become 400s, without attempting the next endpoint unless explicitly specified. - If you want to override this behavior and force trying the next endpoint, add `.onDecodeFailureNextEndpoint` to the input where you expect such handling. See [error handling page](../server/errors.md) for details. ## From 1.2 to 1.3 - Static content endpoints from `sttp.tapir.static._` are deprecated in favor of the new `tapir-files` module. New methods are in `sttp.tapir.files._`: `staticFilesGetServerEndpoint`, `staticFilesHeadServerEndpoint`, `staticFilesServerEndpoints`, `staticResourcesGetServerEndpoint`, `staticResourcesHeadServerEndpoint`, `staticResourcesServerEndpoints`, etc. See the [updated documentation](../endpoint/static.md). - Respectively, use `sttp.tapir.files.FilesOptions` instead of `sttp.tapir.static.FilesOptions` - the `cats` integration module has been split into `cats` and `cats-effect`, with the latter containing the `CatsMonadError` class, providing a bridge between the sttp-internal `MonadError` and the cats-effect `Sync` typeclass. If you've been using this directly, you might need to update your dependencies. ## From 0.20 to 1.0 - `EndpointVerifier` is moved to a separate `tapir-testing` module - `customJsonBody` is renamed to `customCodecJsonBody` - `anyFromStringBody` is renamed to `stringBodyAnyFormat` - `anyFromUtf8StringBody` is renamed to `stringBodyUtf8AnyFormat` - `CustomInterceptors` is renamed to `CustomiseInterceptors` as this better reflects the functionality of the class - `CustomiseInterceptors.errorOutput` is renamed to `.defaultHandlers`, with additional options added. - in custom server interpreters, the `RejectInterecptor` must be now disabled explicitly using `RejectInterceptor.disableWhenSingleEndpoint` when a single endpoint is being interpreted; the `ServerInterpreter` no longer knows about all endpoints, as it is now parametrised with a function which gives the potentially matching endpoints, given a `ServerRequest` - the names of Prometheus and OpenTelemetry metrics have changed; there are now three metrics (requests active, total and duration), instead of the previous 4 (requests active, total, response total and duration). Moreover, the request duration metric includes an additional label - phase (either headers or body), measuring how long it takes to create the headers or the body. - `CustomiseInterceptors.appendInterceptor` is replaced with `.addInterceptor`; `.prependInterceptor` and `.appendInterceptor` methods are also added - `RequestHandler`, returned by `RequestInterceptor`, now also accepts a list of server endpoints. This allows to dynamically filter the endpoints. Moreover, there's a new type parameter in `RequestInterceptor` and `RequestHandler`, `R`, specifying the capabilities required by the given server endpoints. - the http4s server interpreters have only one effect parameter, instead of two (`F` for the general effect and `G` for the body effect). This separation stopped making sense with the introduction of `BodyListener` some time ago and keeping `ServerInterpreter` using a single effect. - the Swagger and Redoc UIs by default use relative paths for yaml/json documentation references and for redirects. This can be changed by passing appropriate options. - The `streamBinaryBody` method now has a mandatory `format` parameter, which previously was fixed to be `CodecFormat.OctetStream()` ### Moved traits, classes, objects - server interpreters & interceptors have moved from `core` into the `server/core` module - `ServerResponse` and `ValuedEndpointOutput` are moved to `sttp.tapir.server.model` - metrics classes and interceptors have moved to the `sttp.tapir.server.metrics` package - `Endpoint.renderPathTemplate` is renamed to `Endpoint.showPathTemplate` - web socket exceptions `UnsupportedWebSocketFrameException` and `WebSocketFrameDecodeFailure` are now in the `sttp.tapir.model` package - OpenAPI and AsyncAPI models are now part of a separate sttp-apispec project, hence the packages of these objects changed as well, from `sttp.tapir.apispec` / `sttp.tapir.openapi` / `sttp.tapir.asyncapi` to `sttp.tapir.apispec.(...)` - server interpreters sources are now grouped based on the underlying server implementation (e.g. http4s, vertx), and then sub-directories contain effect integrations (e.g. cats, zio). Name templates: - for artifacts: `tapir--server-`. E.g. `tapir-zio-http4s-server` became `tapir-http4s-server-zio1` - for package names: `sttp.tapir.server..` - for interpreters: `ServerInterpreter` ## From 0.19 to 0.20 See the [release notes](https://github.com/softwaremill/tapir/releases/tag/v0.20.0) ## From 0.18 to 0.19 See the [release notes](https://github.com/softwaremill/tapir/releases/tag/v0.19.0) ## From 0.17 to 0.18 See the [release notes](https://github.com/softwaremill/tapir/releases/tag/v0.18.0) # Creating your own tapir Tapir uses a number of packages which contain either the data classes for describing endpoints or interpreters of this data (turning endpoints into a server or a client). Importing these packages every time you want to use Tapir may be tedious, that's why each package object inherits all of its functionality from a trait. Hence, it is possible to create your own object which combines all of the required functionalities and provides a single-import whenever you want to use tapir. For example: ```scala object MyTapir extends Tapir with PekkoHttpServerInterpreter with SttpClientInterpreter with OpenAPIDocsInterpreter with SchemaDerivation with TapirJsonCirce with TapirOpenAPICirceYaml with TapirAliases ``` Then, a single `import MyTapir._` and all Tapir data types and interpreter methods will be in scope! You might also define an alias for `Endpoint`, with the capabilities that your endpoints use, e.g.: ```scala import sttp.capabilities.pekko.PekkoStreams import sttp.capabilities.WebSockets import sttp.tapir.Endpoint type MyEndpoint[A, I, E, O] = Endpoint[A, I, E, O, PekkoStreams with WebSockets] ``` # Other interpreters & extensions At its core, Tapir creates a data structure describing the HTTP endpoints. This data structure can be freely interpreted also by code not included in the library. Below is a list of projects, which provide tapir interpreters and extensions to the project. ## GraphQL [Caliban](https://github.com/ghostdogpr/caliban) allows you to easily turn your Tapir endpoints into a GraphQL API. More details in the [documentation](https://ghostdogpr.github.io/caliban/docs/interop.html#tapir). ## tapir-gen [tapir-gen](https://github.com/xplosunn/tapir-gen) extends tapir to do client code generation. The goal is to auto-generate clients in multiple-languages with multiple libraries. [scala-opentracing](https://github.com/Colisweb/scala-opentracing) contains a module which provides a small integration layer that allows you to create traced http endpoints from tapir Endpoint definitions. ## SNUnit [SNUnit](https://github.com/lolgab/snunit) is a Scala Native HTTP Server library based on [NGINX Unit](https://unit.nginx.org/). It provides first-class support for Tapir. ## tapir-http-session [tapir-http-session](https://github.com/SOFTNETWORK-APP/tapir-http-session) provides integration with functionality of [akka-http-session](https://github.com/softwaremill/akka-http-session), which includes client-side session management in web and mobile applications. ## tapir + kyo [Kyo](https://github.com/getkyo/kyo/#routes-http-server-via-tapir) includes a tapir integration module. ## Baku [Baku](https://github.com/arkida39/baku) is a Tapir extension library that allows you to easily isolate your API definitions from server and security logic for cleaner, more maintainable code. This makes it simple to share contracts across microservices without exposing the underlying implementation. # Troubleshooting ## StackOverflowException during compilation Sidenote for scala 2.12.4 and higher: if you encounter an issue with compiling your project because of a `StackOverflowException` related to [this](https://github.com/scala/bug/issues/10604) scala bug, please increase your stack memory. Example: ``` sbt -J-Xss4M clean compile ``` ## Logging of generated macros code For some cases, it may be helpful to examine how generated macros code looks like. To do that, just set an environmental variable and check compilation logs for details. ``` export TAPIR_LOG_GENERATED_CODE=true ``` # 1. Explicit encode function on Validator.Enum Date: 2019-10-03 ## Context To represent enum values in documentation, we need a way to encode them into appropriate raw values. However, codecs and validators are created independently. The codec, to which a validator is added, can be a mapped codec, a product, etc. As codecs are opaque (at least for now), given a codec we don't necessarily have a codec for the wrapped types or fields. Hence, even given a codec, we don't necessarily have an encode method for the value. ## Decision As proposed in [PR240](https://github.com/softwaremill/tapir/pull/240), the `Enum` class will now contain an explicit encode function: ```scala case class Enum[T](possibleValues: List[T], encode: Option[EncodeToAny[T]]) ``` It might seem that another solution would be to require an implicit `Codec` instance when creating the enum validator, however validators are often created as part of codec creation; this would create an infinite loop, e.g.: ```scala implicit def plainCodecForColor: PlainCodec[Color] = { Codec.stringPlainCodecUtf8 .map[Color]({ case "red" => Red case "blue" => Blue })(_.toString.toLowerCase) .validate(Validator.enum) } ``` # 2. Codecs, schemas, validators Date: 2019-11-09 ## Context Schemas for objects need to be customised, adding description/format information so that it's added to the docs. ## Decision First, a definition: `Schema[T]` describes the shape of the low-level, "raw" representation of type `T` A schema is one of the following: a basic schema (string, int, number, boolean, date, ...), an array, binary, object (product / coproduct / open product) or a reference to an object (for recursive schemas). Apart from the information that schema carries with its type, it contains: * a description * a format, giving more low-level details about the representation of the type * optionality ## Customising schemas To customise schemas, the derived schema implicits for case classes (generated by magnolia) are wrapped in a `Derived[T](value: T)` class, with a low-priority implicit converter from `Derived[T]` to `T` (similar to `Exported` in circe). To customise a schema for a given type, we can could do: ```scala implicit val mySchema: Schema[Person] = customise(implicitly[Derived[Person]].value) ``` ## Relationship with validator One of the philosophical questions is, should the validator be part of the schema? Should we derive json encoders/decoders from the schema? Both sound tempting. I think it's worth keeping the divide. On the "cons" side, we have the duplicate recursive logic of building up validators/schemas/encoders for composite types. On the "pro" side however, we have a clear division of responsibility: * schemas describe the low-level shape of the value; however, they don't specify the final format of the values, and are not concerned with constructing/deconstructing objects from raw value tuples, or decode failures * validators describe the subset of values for potentially higher-level types. They are context-free, that is they only constrain values based on the value itself, without knowing the parent object * encoders/decoders translate the value to the low-level format Hence, a validator can influence the schema; the high-level description of the possible values can narrow down the format of the low-level representation, for example. However, not the other way round - it's not generally possible to derive a validator from a schema. Let's consider the `uuid` format and the `UUID` high-level type. In this case, the format is an intrinsic property of the high-level type, no validation apart from decoding is required. ## Why not annotations Adding `@Description` or `@Format` annotations could also be a viable solution, however it has one major drawback: it requires modifying the target datatype. So while this could be an addition to the mechanism described above, it's not a real alternative; we would need a way to change the schemas "by hand" anyway, to be able to describe existing datatypes, which we cannot annotate. ## Update 12/2020 In the end, we added validators to schemas. The reasoning was that once auto- and semi-auto-derivation has been introduced, having to manually derive two structures was highly impractical. Hence, a schema is now a description of the target type `T`, including both the low-level representation, and the high-level validation rules. # 3. Shape of IOs Date: 2019-11-09 ## Context The shape of tapir's inputs and outputs is fixed and in some ways constrained. Below you'll find some motivation behind this design, as well as alternatives. ## Decision The *input* of an endpoint is always a product of values: that is, each new input extends the list of values that the endpoint's input maps to. A new input can contribute 0 values (in case of fixed paths), 1 value (query parameter, path capture, ...), or many values (a composite input). What is *not* possible, is describing a *coproduct*: specifying, that the input is either a value of one type, or a value of another type. Coproducts are harder than products as they need a *discriminator*: some kind of value, basing on which it can be decided, which of the input alternatives to choose. It would be possible to extend tapir and allow coproducts, at the expense of complicating the API. However, use-cases which require such mappings are rare (or so it seems), so this isn't currently implemented. And there's always a "back door": an alternative of values can be described as a "flattened" product of optional values, with additional input validation. *Outputs* are a bit more complicated. First of all, success and error outputs are separate. This defines a top-level coproduct, where the output is either mapped to the branch error, or the success branch. The discriminator in this case is the status code. However, both error and success outputs can contain coproducts as well, using the `oneOf` output. The `oneOf` output specifies a number of alternative outputs, again discriminated using fixed status code values (which is important to be able to generate documentation). The types, to which the branches should map, have to form an inheritance hierarchy, e.g.: ```scala import sttp.model.StatusCode import sttp.tapir._ import sttp.tapir.json.circe._ import sttp.tapir.generic.auto._ import io.circe.generic.auto._ sealed trait ErrorInfo case class NotFound(what: String) extends ErrorInfo case class Unauthorized(realm: String) extends ErrorInfo case class Unknown(code: Int, msg: String) extends ErrorInfo val baseEndpoint = endpoint.errorOut( oneOf( oneOfVariant(StatusCode.NotFound, jsonBody[NotFound].description("not found")), oneOfVariant(StatusCode.Unauthorized, jsonBody[Unauthorized]), oneOfDefaultVariant(jsonBody[Unknown].description("unknown")) ) ) ``` Again, this could be generalised to allow other discriminators (e.g. on fixed header values), however there are no compelling use-cases which would justify this. It would also be possible to generalise the error/success outputs into a single output type. Users could then use the `oneOf` output to differentiate between errors and successes, for example: ```scala val e1: Endpoint[Unit, Either[String, Book], Nothing] = endpoint .out(either( statusCode(200) -> jsonBody[Book], statusCode(400) -> stringBody )) val e2: Endpoint[Unit, Either[ErrorInfo, Book], Nothing] = endpoint .out(either( statusCode(200) -> jsonBody[Book], otherwise -> oneOf[ErrorInfo]( statusCode(404) -> jsonBody[NotFound], statusCode(403) -> jsonBody[Unauthorized], statusCode(400) -> jsonBody[Unknown] ))) ``` However, error outputs are almost always *different* from success outputs, so it's worth complicating the API to support this distinction as a first-class construct. Moreover, quite often endpoints share the error output description, while the success output vary from endpoint to endpoint. # 4. Codecs parametrised by raw values Date: 2019-11-22 ## Context Currently, tapir's `Codec` is parametrised by: the high-level type, the codec format and the raw value (one of the supported ones, as defined by the `RawValueType` family). Can we drop the third parameter and just have `Codec[T, CF]`? ## Decision In short: yes, but at the expense of some type safety. In some contexts, we need to know that the raw value should be a `String`: in headers, query parameters and the path. Hence, we need this restriction in place. In theory we could also accept other codecs and convert to/from strings as necessary. This could be done e.g. for byte arrays; however the question the is which charset to choose when converting a `String` to an `Array[Byte]` to be consumed by the codec? Also, we would need to throw exceptions when the raw value would be a file or a multipart, however cases where users would try to use e.g. a multipart codec for a header would be extremely rare, if ever. Moreover, if somebody wanted to use the codecs directly (to encode/decode), the result would be an abstract `R` type, instead of a specific one. This could make debugging, exploration or codec re-use harder. User exposure to the `Codec` type should be limited anyway, as usually codecs for custom types are either derived automatically, or customised basing on existing ones, for usage in headers/query parameters/path. In the latter case, the raw type is usually `String` and the format `TextPlain`, in which case the single-parameter type alias, `PlainCodec`, can be used. That's why we keep the current design as-is. # 5. Rethinking codecs Date: 2020-05-01 ## Context Current codecs suffer from a couple of implementation issues: they are not composable; mapping an input/output has less capabilities than mapping a codec; there are multiple types of codecs; encodings are specified in two places, and it's not clear which one to pick (from the codec format or from the raw value type). That's why the `Codec` class has been entirely redeisgned. ## Decision A new base trait, `Mapping[L, H]`, is introduced, which establishes a correspondence between low-level values of type `L` and high-level values of type `H`. Decoding `L` to `H` might signal a decoding error. This trait is extended by `Codec[L, H, CF]`. The third type parameter, codec format, differentiates between codecs for same types, but different content types. It also provides a default value for the content type, if it's not specified explicitly. An input requires a codec either from a single fixed type (such as a `String`), or from multiple fixed types (corresponding to one of `RawValueType`s - in case of bodies). However, codecs and mappings might also exist and take part in composition between any other two types. The requirement is just on the final, composed form, which is used when defining inputs/outputs. String encodings are determined basing on context (for headers/query parameters/path segments), or basing on the raw value type. The `RawValueType` is the only place, which can provide custom encoding to convert between a `String` and bytes to be sent over the network. Codecs also can contain schemas and validators. When extending a codec using `.map` with a subsequent codec or mapping, the other schema/validator is discarded - the RHS is always treated as a plain `Mapping`. This might require overriding schemas/validators by hand, but is less surprising as calling `.map` using a `Mapping` and using a `Codec` always does the same thing. Codecs for optional and multiple elements can be automatically derived from codecs for single elements, reporting decoding errors if the arity of values doesn't match. Finally, codecs for inputs/outputs are still looked up implicitly, with the user-provided codecs for simple types being automatically converted to required optional/list types. # 6. Partial server logic Date: 2020-06-01 ## Context The server logic often should be provided in parts - e.g. common authentication logic should be extracted into a reusable function. ## Decision More power is given to `ServerEndpoint`. There are now three variants of the class: 1. where the entire server logic is provided 2. where the server logic for the entire inputs defined so far is provided 3. where the server logic for some inputs defined so far is provided In case of 2., the endpoint can be extended with further inputs/outputs, and further logic fragments (again for the entire input defined so far) can be given. It's not possible to combine 2. and 3. without an explosion of type parameters (of which there already are many). Providing the server logic only for some inputs, while maintaining the capability of extending the input list, would require a lot of type-level computations, and tracking a lot of intermediate variables. # 7. Codecs and schemas derivation configs Date: 2020-10-26 ## Context In circe and probably in other json libraries there is an option to influence generic derivation by modifying derivation config and putting it into the derivation scope. Such modification will affect particular codecs but won't propagate automatically into openapi schema generated by tapir. These modifications include choosing the namingConvention or specifying a discriminator field. (see https://github.com/softwaremill/tapir/issues/315 for more details) ## Decision Tapir's schema and json codes will be treated separately and require manual synchronization. ## Other alternatives which were considered 1. Create a super configuration from which tapir and circe configurations can be derived. This approach has a few flaws. First, for circe we would have to create another integration module as circe-generic isn't present in the basic one. Second, it isn't uncommon for codecs and schemas to live in separate packages and that brings up the question of where to put this super configuration. Last but not least, this configuration would require additional import in both codecs and schemas files which could be easily overlooked, so in summary we think that it doesn't bring that much value to a table. (see https://github.com/softwaremill/tapir/pull/465 for details) # 8. No effectful maps Date: 2021-01-26 ## Context Currently any mapping of endpoint inputs and outputs can't have side effects, that is we only allow simple functions `T => U` & `U => T` to define the mapping. It would be possible to add effectful mappings by extending the endpoint input/output with a capabilities type parameter. Any endpoints mapped with an effect would require the `Effect[F]` capability, which would have to be supported by the interpreter. ## Decision This could be implemented as a family of functions, in the most general form: ```scala trait EndpointInput[T, -R] { // ... def mapF[F[_], U](f: T => F[U])(g: U => F[T]): EndpointInput[U, R with Effect[F]] = ??? } ``` However, the utility of such effectful maps seems limited. One of the possible use-cases would be to map an id-input to the entity, looked up from the database. For example, if we've got users who are identified using `Long` ids, we would like to convert an `EndpointInput[Long]` into a `EndpointInput[User]`. For that, we would need a function: `Long => F[User]` and a trivial one `User => F[Long]`, which would just return the id wrapped in the effect. However: 1. Typically, we don't have a function `Long => F[User]`, but a `Long => F[Either[Error, User]]`. We could represent errors as failed effects and use failure handlers to return responses, but that's a non-type-safe and a global solution. Note that when using e.g. `.serverLogicForCurrent` this is already taken care of, as the result can be an error, which is mapped to the endpoint's errors. 2. The mapping seems only to be useful on the server side. When using an endpoint as a client, we probably don't have the `User` instance, but only the id. Hence, we wouldn't be able to provide the value necessary for the client call anyway. Summing up, it seems that the current solution with two approaches to partially defining the server logic seem sufficient and better. As no other compelling use-cases have been identified, we're keeping the current design as-is, without introducing effectful mappings. # 9. Lists are optional Date: 2021-09-08 ## Context When deriving or implicitly inferring a schema for a collection-value type `T` (e.g. `List[String]`), should the resulting `Schema[T]` be optional, or not? That is, what should be the value of the `Schema[T].isOptional` field? On one hand, the collection can be empty, just as an `Option[T]` can be empty, which would indicate that this value is optional. On the other, when a json object contains a collection-valued field, there are four possible states: an entirely missing field, a field with a `null` value, an empty array `[]`, or a non-empty array. In this scenario, one could consider `List[String]` to allow only empty and non-empty array values, while `Option[List[String]]` would allow all four. ## Decision Schemas for collection-valued types are optional. This is because when deriving/inferring schemas, there is no context, that is, we don't know whether we are deriving a schema for a query parameter, a header, or a json field. In many cases, making collection schemas optional is the only sensible choice: e.g. `query[List[String]]("x")` is obviously optional, as there are only two possible states: either there are no query parameters with name `x`, or there are multiple ones. Similar for headers. Even for body values, this often makes the same sense: when parsing the body as a comma-separated list of values, or when generating the schema for an xml body, where children elements can be repeated or not (there are no `null` values). This is different for json, though, as a field can be absent or `null`-valued. In the current representation, `Schema`s make no distinction between a missing field, or an empty array. Json parsers might behave differently - though this isn't something that can be generically supported; so either the json parsers, or the schemas will have to be adjusted accordingly. As a work-around, if needed, there are several options: * use a dedicated type, for which a schema can be separately defined, just as the schema for `NonEmptyList` from the cats integration is required * define a local implicit: ```scala implicit def schemaForIterable[T: Schema, C[X] <: Iterable[X]]: Schema[C[T]] = implicitly[Schema[T]].asIterable[C].copy(isOptional = false) ``` when deriving the schema for a type that is later serialised to json, e.g. in a block in which `jsonBody` is called. * [customise](https://tapir.softwaremill.com/en/latest/endpoint/schemas.html#customising-derived-schemas) the derived schemas # 10. Security refactoring Date: 2021-11-03 ## Context Tapir's support for security-related features until version 0.18 is quite limited. There are `auth` inputs, which define authentication credential metadata, used when generating documentation. There also is the possibility to define the server logic either: * for a fully defined endpoint, incrementally in parts * for a partially defined endpoint, for all inputs defined so far However, this implementation has some drawbacks: * all inputs are always decoded (including the body) before any logic is run, regardless whether the logic is defined all at once, or in parts * defining a partial endpoint with the authentication logic is possible, however the resulting type is complex * it's confusing as to what's the difference between providing the server logic in parts for a fully defined endpoint, vs. defining a partial endpoint * for cases where you don't want to create an endpoint with partial logic, it's not possible to enforce, or at least clearly communicate that some security logic should be run ## Decision Security should be one of the central concepts for any HTTP library, so it is necessary to improve this area in tapir. There were several design directions, which haven't been chosen: * `DefferedServerEndpoint`, documented in the related [GitHub issue](https://github.com/softwaremill/tapir/issues/1167), which inverts the order in which the security & main logic are provided, comparing to the existing partial server logic feature. This, however, doesn't really solve any of the issues. * creating a security interceptor, which would run the given security logic for endpoints tagged as "secure"; the computed "user" instance (if authentication is successful) would be placed in a server request attribute, and could later be extracted using a server-only input. Apart from the necessity of adding server attributes, and enriching the secure endpoints with an input but only when interpreted as a server, the main problem with this implementation was deciding if the security logic should be run, given an endpoint. The security logic from the interceptor should run only if the endpoint "matches" the request - which means we need to decode the inputs; however, we don't want to decode the body, which means that the interceptor would have to deeply interfere with the inner workings of the `ServerInterpreter`. Possibly, a dedicated callback would be needed, making the whole process fragile and complex. Not to mention problems with passing the authentication credential metadata to generate documentation, or making proper client calls. * creating a parallel `SecureEndpoint[A, I, E, O, R]` class, which would have dedicated security-related inputs, next to `Endpoint[A, I, E, O, R]`. We would need a similar distinction for `ServerEndpoint`. However, converting a type that is central to tapir, `Endpoint`, from a case class to a sealed trait family would complicate the codebase and endpoint usage significantly. Instead, we decided to extend the base `Endpoint` type with dedicated security inputs, adding a type parameter (which is an unfortunate, but necessary consequence). Similarly, `ServerEndpoint` gathered two new parameters: the type of the security inputs, and the type of the values returned by a successful invocation of the security logic. This is a major source-level breaking change, but to properly support security concerns, it had to be done. For endpoints without security defined, a type alias is introduced - `PublicEndpoint`, which fixed the type of the security inputs to `Unit`. This corresponds to the old type of endpoints - hence for the migration period, the type of endpoints can be changed to `PublicEndpoint` and everything should work the same. When interpreting an endpoint as a server, first the basic inputs (that is path, query, headers, without the body) are decoded, to verify that the endpoint matches the request - this solves the major problem that we had with the interceptor approach. Only when the inputs decode successfully, first the security logic is run, then the body is decoded, and finally the main logic is run, given the value of the result of the security logic and the decoded inputs. An endpoint with security inputs defined clearly communicates, that some form of security logic needs to be provided. In fact, the only way to provide server logic for secure endpoints is a two-step process: first the security logic using `.serverSecurityLogic`, then the main logic using `.serverLogic`. For public endpoints, it's possible to simply call `.serverLogic`. ### Partial server endpoints Partial endpoints can be defined in a similar way. After providing the security logic using `.serverSecurityLogic`, additional inputs and outputs can be defined. This way, a base "secure" endpoint can be defined, and later extended. This approach also works seamlessly with client and documentation interpreters. This two-stage approach of providing server logic is less flexible that the previous `.serverLogicForCurrent`, however it more clearly defines the intent, and is less complicated when it comes to type-level computations on tuples. Hence, it should be easier to understand. Providing the server logic in parts is also somewhat implemented using the two-stage server logic approach. Similarly, this is now limited to maximum two stages. If the main server logic is fragmented into multiple pieces, it's up to the user to combine these functions into a single one. ### Auth inputs Given that there's a dedicated section for security inputs, should we keep the distinct auth inputs (e.g. `auth.bearer` or `auth.apiKey`)? It could seem that this is no longer necessary - after all, all inputs defined in the security section can be assumed to provide authentication credentials. However, there's still a need to provide meta-data for security-related inputs, such as naming security schemes for OpenAPI, or providing appropriate `WWW-Authenticate` challenges. Hence, if users wish to provide this meta-data, some kind of endpoint input wrapper is still needed. Secondly, it's convenient having helper methods such as `auth.bearer`, where the `auth` objects groups all authentication-related inputs and provides them with the proper codecs and meta-data. Finally, it is possible that users wish to capture more data as part of the security inputs, in addition to the authentication credentials - such as fixed path segments, variable path segments, additional query parameters etc. Hence, the authentication-credential inputs remain unchanged, at least for the time being. # 11. Dedicated schema macros Date: 2022-05-16 ## Context We've been considering replacing the Magnolia-base derivation with custom `Schema` macros for three main reasons: 1. allow additional compile-time validation, such as whether validators are added properly (https://github.com/softwaremill/tapir/issues/2110) 2. provide better Schema-dedicated error reports, to improve debuggability of schema derivation 3. potentially (not verified) speed up the compilation process (as we directly generate the schemas at compile-time, without the intermediary Magnolia representation and proper derivation happening at run-time) 4. potentially (not verified) decrease the size of generated code (as we only need a fraction of the meta-data that is generated by Magnolia) Switching the way schemas are derived would need to be done before 1.0, as generating code without a Magnolia dependency would break backwards compatibility. ## Decision Two PoCs have been implemented, one for Scala2, and one for Scala3, on the following branches: * https://github.com/softwaremill/tapir/tree/scala2-schema-experiment (Scala2) * https://github.com/softwaremill/tapir/tree/schema-experiments (Scala3) However, the Scala2 version would require re-implementing (or copying) large parts of Magnolia to handle recursive derivation in both auto and semi-auto modes. Because of little verified benefit, the experiment was put on hold. The Scala3 version got stuck due to compiler bugs (https://github.com/lampepfl/dotty/discussions/15157, https://github.com/lampepfl/dotty/issues/15159), so possible future work is possible, when the above is fixed. We might initially release a backwards-compatible-guaranteed version only for Scala2, which would leave more room to improve the Scala3 variant. # 12. Extracting schema as a module Date: 2022-05-16 ## Context As part of making the `core` module smaller, we've been considering splitting out `schema` to a separate module, on which `core` would depend. This would not only make `core` leaner, but also potentially allow others to use `schema` without depending on the entire `core`. ## Decision Doing such a move would require removing `Part`, `FileRange`, enumeration validator helper methods out of the companion objects of `Schema` and `Validator`; or - adding a dependency on `sttp-model` to the extracted project. The first would decrease the "programmer experience" while using tapir, the second would make the whole operation impractical (as one of the goals was to create a module with a smaller number of dependencies). As benefits are not immediately apparent, and there are no known use-cases for an extracted `schema` module, this change was not introduced. # SoftwareMill launches stable release of tapir, making it easier than ever for HTTP API developers to benefit from Scala's expressivity **Warsaw, Poland – June, 2022 – SoftwareMill S.A.** After almost 4 years of development and multiple 0.x releases, SoftwareMill is happy to announce the release of tapir 1.0! The goal of the [tapir library](https://tapir.softwaremill.com/) is to provide a programmer-friendly, reasonably type-safe API to expose, consume and document HTTP endpoints, using the Scala language. > “With tapir, you can describe an endpoint as an immutable Scala value. This description and its parts can be arbitrarily extended and reused, making it possible to leverage the abstraction capabilities given by the Scala language.” says Adam Warski, CTO at SoftwareMill. “This is in contrast to the more "traditional" annotation-driven HTTP frameworks, which are severely constrained when it comes to even simple refactorings, such as extracting common code.” Such a description - capturing the complete metadata of the endpoint - can be then interpreted in three basic ways: as a server, a client, or as documentation. ## Fitted into Scala ecosystem Any of the leading Scala programming styles, that is `Future`-based, using cats-effect or ZIO can be used. Tapir integrates with a number of existing server implementations, such as akka-http, http4s, vertx or the Play framework. When generating documentation, tapir can generate “raw” OpenAPI yaml files, and expose them using the SwaggerUI or Redoc. AsyncAPI is supported out-of-the-box as well. Further integrations include observability tools, which take advantage of the tapir-provided metadata, to enrich metrics, logs or traces. ## Ready to facilitate smooth collaboration Compile-time verification combined with type-driven auto-complete in the IDE allows shortening of feedback loop cycles during development. On the other hand, leveraging the rich endpoint metadata, which is available as an ordinary Scala value, allows smooth collaboration with other teams who want to consume the API being developed, or provide other APIs for consumption. ## Get started now If you’d like to try tapir, check out our [documentation](https://tapir.softwaremill.com/en/latest/) or the [adopt-tapir page](https://adopt-tapir.softwaremill.com), where you can generate a customised bare-bones tapir project. You can also generate a stub of a tapir-based application directly from the command line with `sbt new softwaremill/tapir.g8`. Looking forward to learning about your impressions of the library! ## Additional Resources * [scala.page](https://softwaremill.com/scala/) * [SoftwareMill Tech Blog](https://softwaremill.com/blog/) * [Scala Times](https://scalatimes.com/) ## About SoftwareMill We help clients scale their business through software, conduct digital transformation, implement event sourcing and create data processing pipelines. We specialise in Scala, Kafka, Akka and Cassandra, among other technologies. Our areas of expertise include distributed systems, big data, blockchain, machine learning and data analytics. Project in trouble? We'll help your team.