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) 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 typeT- 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 typeEndpointInput[Unit]) paths, which maps to the whole remaining path as aList[String]query[T](name)captures a query parameter with the given namequeryParamscaptures all query parameters, represented asQueryParamscookie[T](name)captures a cookie from theCookieheader with the given nameextractFromRequestextracts 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 theunderlying: Anyfield.
For both inputs/outputs:
header[T](name)captures a header with the given nameheaderscaptures all headers, represented asList[Header]cookiescaptures cookies from theCookieheader and represents them asList[Cookie]setCookie(name)captures the value & metadata of the aSet-Cookieheader with a matching namesetCookiescaptures cookies from theSet-Cookieheader and represents them asList[SetCookie]stringBody,plainBody[T],jsonBody[T],rawBinaryBody[R],binaryBody[R, T],formBody[T],multipartBody[T]captures the bodystreamBody[S]captures the body as a stream: only a client/server interpreter supporting streams of typeScan be used with such an endpoint
For outputs:
statusCodemaps to the status code of the responsestatusCode(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:
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: Endpoint[(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:
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: Endpoint[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:
case class Status(uptime: Long)
val statusEndpoint: Endpoint[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:
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 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[CaseClass] 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).
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
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
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.annotations for describing both input and output values:
@headercaptures 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@headerscaptures all headers. Can only be applied to fields represented asList[Header]@cookiescaptures all cookies. Can only be applied to fields represented asList[Cookie]@jsonbodycaptures JSON body of request or response. Can only be applied to field if there is implicit JSONCodecinstance fromStringto target type@xmlbodycaptures XML body of request or response. Also requires implicit XMLCodecinstance fromStringto target type
Following annotations are only available for describing input values:
@querycaptures a query parameter with the same name as name of annotated field in a case class. The same as annotation@headerit has optional parameter to specify alternative name for query parameter@paramscaptures all query parameters. Can only be applied to fields represented asQueryParams@cookiecaptures a cookie with the same name as name of annotated field in a case class. The same as annotation@headerit has optional parameter to specify alternative name for cookie@apikeywraps any other input and designates it as an API key. Can only be used with another annotations@basicextracts data from theAuthorizationheader. Can only be applied for field represented asUsernamePassword@bearerextracts data from theAuthorizationheader removing theBearerprefix.@pathcaptures a path segment. Can only be applied to field of a case class if this case class is annotated by annotation@endpointInput. For example,
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:
@setCookiesends value in headerSet-Cookie. The same as annotation@headerit has optional parameter to specify alternative name for cookie. Can only be applied for field represented asCookieValueWithMeta@setCookiessends severalSet-Cookieheaders. Can only be applied for field represented asList[Cookie]@statusCodesets status code for response. Can only be applied for field represented asStatusCode
Path matching¶
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, /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.
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)".
Next¶
Read on about status codes.