Enumerations

tapir supports both scala.Enumeration-based enumerations, as well as enumerations created as a sealed family of objects (in Scala 2) or Scala 3 enums where all cases are parameterless. Other enumeration implementations are also supported by integrating with third-party libraries.

Depending on the context, in which an enumeration is used, you’ll need to create either a Schema, or a Codec (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 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:

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:

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:

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
}

implicit val featureCodec: 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:

import sttp.tapir._
import sttp.tapir.Codec.PlainCodec

sealed trait Color
case object Blue extends Color
case object Red extends Color

implicit val colorCodec: 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 by hand is exactly the same as for any other type. The only difference is that an enumeration validator 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:

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

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

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
implicit val enumDecoder: Decoder[Features.Feature] = Decoder.decodeEnumeration(Features)
implicit val enumEncoder: 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:

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")
}

implicit val customFeatureSchema: 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 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:

import sttp.tapir._

sealed trait Feature
object Feature {
  case object A extends Feature
  case object B extends Feature
  case object C extends Feature
}

implicit val featureSchema: Schema[Feature] = 
  Schema.derivedEnumeration[Feature].defaultStringBased

Similarly, using Scala 3’s enums:

enum ColorEnum {
  case Green extends ColorEnum
  case Pink extends ColorEnum
}

given Schema[ColorEnum] = Schema.derivedEnumeration.defaultStringBased

Creating an enum schema by hand

Creating an enumeration schema by hand is exactly the same as for any other type. The only difference is that an enumeration validator has to be added to the schema.

Next

Read on about validation.