Datatypes integrations


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 datatypes as well as additional syntax:

"com.softwaremill.sttp.tapir" %% "tapir-cats" % "1.9.10"
  • 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:

"com.softwaremill.sttp.tapir" %% "tapir-cats-effect" % "1.9.10"

Refined integration

If you use refined, the tapir-refined module will provide implicit codecs and validators for T Refined P as long as a codec for T already exists:

"com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.9.10"

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, the tapir-iron module will provide implicit codecs and validators for T :| P as long as a codec for T already exists:

"com.softwaremill.sttp.tapir" %% "tapir-iron" % "1.9.10"

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


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 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:

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:

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](

def ironDecodeFailureInterceptor[T[_]] = new DecodeFailureInterceptor[T](ironFailureHandler[T])

…and add it to server options:

override def run = NettyCatsServer
  .use { server =>
    // Don't forget to add the interceptor to server options
    val optionsWithInterceptor = server.options.prependInterceptor(ironDecodeFailureInterceptor)
    for {
      binding <- server

Enumeratum integration

The tapir-enumeratum module provides schemas, validators and codecs for Enumeratum enumerations. To use, add the following dependency:

"com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % "1.9.10"

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, 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:

"com.softwaremill.sttp.tapir" %% "tapir-newtype" % "1.9.10"

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, 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:

"com.softwaremill.sttp.tapir" %% "tapir-monix-newtype" % "1.9.10"

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, 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:

"com.softwaremill.sttp.tapir" %% "tapir-zio-prelude" % "1.9.10"

Then, mix in sttp.tapir.codec.zio.prelude.newtype.TapirNewtypeSupport into your newtype to bring the implicit values into scope:

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


Or use the TapirNewtype helper to derive a codec or a schema without modifying the newtype:

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._

Derevo integration

The tapir-derevo module provides a way to derive schema for your type using @derive annotation. For details refer to derevo documentation. To use, add the following dependency:

"com.softwaremill.sttp.tapir" %% "tapir-derevo" % "1.9.10"

Then you can derive schema for your ADT along with other typeclasses besides ADT declaration itself:

import derevo.derive
import sttp.tapir.derevo.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 or supertagged:

import derevo.derive
import sttp.tapir.derevo.schema
import io.estatico.newtype.macros.newtype

object types {
  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.


Read on about serving static content.