An input is described by an instance of the
EndpointInput trait, and an output by an instance of the
trait. Some inputs can be used both as inputs and outputs; then, they additionally implement the
Each input or output can yield/accept a value (but doesn’t have to).
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) as an
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
- any string, which will be implicitly converted to a fixed path segment. Path segments can be combined with the
/method, and don’t map to any values (have type
paths, which maps to the whole remaining path as a
query[T](name)captures a query parameter with the given name
queryParamscaptures all query parameters, represented as
cookie[T](name)captures a cookie from the
Cookieheader with the given name
extractFromRequestextracts a value from the request. This input is only used by server interpreters, ignored by documentation interpreters. Client interpreters ignore the provided value.
For both inputs/outputs:
header[T](name)captures a header with the given name
headerscaptures all headers, represented as
cookiescaptures cookies from the
Cookieheader and represents them as
setCookie(name)captures the value & metadata of the a
Set-Cookieheader with a matching name
setCookiescaptures cookies from the
Set-Cookieheader and represents them as
multipartBody[T]captures the body
streamBody[S]captures the body as a stream: only a client/server interpreter supporting streams of type
Scan be used with such an endpoint
statusCodemaps 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
start (mandatory) and
limit (optional) can be written down as:
import sttp.tapir._ 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: Endpoint[(UUID, Option[Int]), Unit, List[User], Nothing] = endpoint.in("user" / "list").in(paging).out(jsonBody[List[User]])
Second, inputs can be combined by calling the
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:
import sttp.tapir._ import sttp.tapir.json.circe._ import io.circe.generic.auto._ case class ErrorInfo(message: String) val baseEndpoint: Endpoint[Unit, ErrorInfo, Unit, Nothing] = 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:
case class Status(uptime: Long) val statusEndpoint: Endpoint[Unit, ErrorInfo, Status, Nothing] = baseEndpoint.in("status").out(jsonBody[Status])
The above endpoint will correspond to the
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:
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
Mappings can also be done given an
Mapping[I, II] instance. More on that in the secion on codecs.
Creating a mapping between a tuple and a case class is a common operation, hence there’s also a
mapTo(CaseClassCompanion) method, which automatically provides the functions to construct/deconstruct the case class:
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).
Endpoint.mapInTo etc. have the same signatures are the ones above.
By default (as with all other types of inputs), if no path input/path segments are defined, any path will match.
If any path input/path segment is defined, the path must match exactly - any remaining path segments will cause the
endpoint not to match the request. For example,
endpoint.in("api") will match
/api/, but won’t match
To match only the root path, use an empty string:
endpoint.in("") will match
To match a path prefix, first define inputs which match the path prefix, and then capture any remaining part using
endpoint.in("api" / "download").in(paths)".
Both input and output bodies can be mapped to a stream, by using
streamBody[S]. The type
S must match the type of
streams that are supported by the interpreter: refer to the documentation of server/client interpreters for the
Adding a stream body input/output influences both the type of the input/output, as well as the 4th type parameter
Endpoint, which specifies the requirements regarding supported stream types for interpreters.
When using a stream body, the schema (for documentation) and format (media type) of the body must be provided by hand,
as they cannot be inferred from the raw stream type. For example, to specify that the output is an akka-stream, which
is a (presumably large) serialised list of json objects mapping to the
import sttp.tapir._ import akka.stream.scaladsl._ import akka.util.ByteString case class Person(name: String) endpoint.out(streamBody[Source[ByteString, Any]](schemaFor[List[Person]], CodecFormat.Json()))
See also the runnable streaming example.