The releases of sttp client 2.0.0-RCx and sttp tapir 0.12 bring some important (and breaking) changes. They share the sttp namespace, which now simply stands for “Scala-related HTTP projects”. This means package name changes, group id changes, but also a shared HTTP model, which is one of the driving reasons for the change and enables smoother interoperability between the two projects.

There are also other changes — some of them not entirely mechanical. They are covered in both sttp client’s and sttp tapir’s release notes, but let’s briefly cover them here as well. Moreover, this Bootzooka commit might also be an illustrative example of the changes that are required.

Migrating geese

sttp client 2.x

The first change that you’ll need to make is update the dependency coordinates. What was before "com.softwaremill.sttp" %% "core" % "1.x" , now becomes (the group id has changed):

"com.softwaremill.sttp.client" %% "core" % "2.x"

Similar mechanical change is required for package names. Before, imports looked like this: import com.softwaremill.sttp._ . Now:

import sttp.client._

Finally, before the starting request was simply called sttp . It wasn’t the best choice, as it clashes with the package name. Hence, now, the starting request is:

basicRequest: RequestT[Empty, Either[String, String], Nothing]

But that’s not all! Now it’s time for the “logical” changes.

Closing backends

An important change is that in all backends, the close() method now returns an effect, e.g.: Future[Unit] or Task[Unit] . If the effect is lazily evaluated (such as Task or IO ), make sure to include it in your final effect!

Allocating backends

Creating “functional” backends is now an effect as well. This affects Monix, ZIO and cats-effect ones. The factory methods return the backend wrapped in the appropriate effect. Moreover, for cats-effect, there’s a variant of the factory method which returns a Resource .

For example:

AsyncHttpClientMonixBackend.apply:

(SttpBackendOptions, BoundRequestBuilder) =>

Task[SttpBackend[Task, Observable[ByteBuffer], WebSocketHandler]]

Websockets

Websocket support is a new feature in sttp 2.x, but it affects existing backend and especially backend wrappers.

First, the backend now has an additional type parameter ( WS_HANDLER ). If you don’t need to use websockets, use NothingT when fixing existing signatures for backends, e.g.:

val myBackend: SttpBackend[Future, Nothing, NothingT] = ...

If you do need websocket support, use the type parameter appropriate for the backend.

For backend wrappers, the SttpBackend trait has an additional openWebsocket method, which must be properly implemented in a wrapper. Very often it will be a simple pass-through to the delegate, but it can also have some logic, e.g. in an error-logging wrapper:

Response-as changes

One of the biggest changes is how response handling is specified. Before, sttp client assumed that any errors should be deserialized as a Byte[Array] , or a String . Now, this constraint is no more there. Responses can be deserialized differently basing on the response status code, but it’s not a requirement.

However, this means that both error & success have to be represented in the type signature. Hence, instead of e.g. ResponseAs[String] , we now have ResponseAs[Either[String, String]] .

In a similar way, instead of a Response[String] , we now have Response[Either[String, String]] . The default response specifications, e.g. basicRequest.response(asString) have had their types adjusted accordingly; hence, you should only have to change the types, but not the code.

But you can also get more flexibility by using e.g. asStringAlways: ResponseAs[String] , or dynamically deciding on response handling using:

def fromMetadata[T, S](

f: ResponseMetadata => ResponseAs[T, S]): ResponseAs[T, S]

This change also means that parseResponseIf no longer needed, as these conditions can be expressed using ResponseAs combinators.

Moreover, there’s no Response.unsafeBody , as we make no assumptions over the shape of the response’s body (so we don’t even know if it’s an Either at all, as was the case before).

Less cats conflicts

Some sttp client imports conflicted with cats, so to reduce the surface area of default imports, sttp’s MonadError has been moved to a separate package, and Id renamed to Identity .

JSON changes

All json-parsing ResponseAs specifications now return an Either[ResponseError, B] for consistency (errors are represented as values, not exceptions). There are also other json-parsing variants:

def asJson[B]: ResponseAs[Either[ResponseError[E], B], Nothing]

def asJsonAlways[B]: ResponseAs[Either[DeserializationError[E], B], Nothing]

def asJsonAlwaysUnsafe[B]: ResponseAs[B, Nothing]

sttp model

sttp model is a new project, which will soon be moved to a separate repository. Status , Method , Header , MediaType , Uri have been moved and merged from sttp client or sttp tapir to a common sttp.model package.

The model classes share common conventions for parsing and serialisation:

.toString returns a representation of the model class in a format as in an HTTP request/response. For example, for an uri this will be http://... , for a header [name]: [value] , etc.

returns a representation of the model class in a format as in an HTTP request/response. For example, for an uri this will be , for a header , etc. constructors of the model classes are private; instances should be created through methods on the companion objects.

[SthCompanionObject].parse(serialized: String): Either[String, Sth] : returns an error message or an instance of the model class

: returns an error message or an instance of the model class [SthCompanionObject].unsafeApply(values) : creates an instance of the model class; validates the input values and in case of an error, throws an exception. An error could be e.g. that the input values contain characters outside of the allowed range

: creates an instance of the model class; validates the input values and in case of an error, throws an exception. An error could be e.g. that the input values contain characters outside of the allowed range [SthCompanionObject].safeApply(...): Either[String, Sth] : same as above, but doesn't throw exceptions. Instead, returns an error message or the model class instance

: same as above, but doesn't throw exceptions. Instead, returns an error message or the model class instance [SthCompanionObject].notValidated(...): Sth : creates the model type, without validation, and without throwing exceptions

HeaderNames , StatusCode and Method companion objects contain enumerations for well-known values of these methods. Header also contains method for creating some well-known header types.

The model is currently quite simple, but complex enough for the needs of sttp client and tapir. It’s not as type-safe as in akka-http and http4s, but still provides basic type safety, and can be used with little friction.

sttp tapir

Like before, the first change is updating the dependency coordinates. What was before "com.softwaremill.tapir" %% "core" % "0.11.x" , now becomes (the group id has changed):

"com.softwaremill.sttp.tapir" %% "core" % "0.12.x"

Similar mechanical change is required for package names. Before, imports looked like this: import tapir._ . Now:

import sttp.tapir._

Codec formats

If you have any custom Codec s, they are now parametrised with a CodecFormat , instead of directly with a MediaType . The names of the supported formats haven’t changed, though, so again this should be a mechanical change of the used companion object.

This change allows decoupling the formats used in tapir from the media types defined in the model. Still, however, a format corresponds to a sttp.model.MediaType .

Schemas

Before, “raw” schemas were defined using the Schema family of case classes, and a schema for a specific type could be specified using SchemaFor[T] . Now, this former has been renamed to SchemaType , and the latter to Schema[T] .

Moreover, schemas carry additional metadata, such as descriptions or customisable formats, which then end up in the OpenAPI documentation. Schemas can be customised by looking up implicit values of type Derived[Schema[T]] . See the documentation for more details on how to auto-derive schemas.

Customising decode failures

If decoding of an input fails (e.g. parsing a query parameters, or a missing required header), the server interpreter can either try the next endpoint (if any, or return a 404 ), or it can return a 400 Bad Request with some details on the source of the error, and optional detail.

This is specified by customising the DecodeFailureHandler function, which is specified in AkkaHttpServerOptions or Http4sServerOptions . The functionality remains, however the way customisation is done has changed: it’s now easier to override individual components of the default behaviour, but also still possible to plug a completely custom one.

Summary

Hopefully the majority of changes will be mechanical ones, and won’t cause too many problems when migrating codebases. In case the information above wouldn’t be sufficient, don’t hesitate to create issues on the GitHub projects, or join our Gitter rooms: