17

I am using Scala and Circe. I have the following sealed trait.

  sealed trait Mode
  case object Authentication extends Mode
  case object Ocr extends Mode

The output of this case object when called SessionModel.Authentication is the following:

"Authentication":{}

I need to convert this to a string so it outputs "authentication"

Kay
  • 17,906
  • 63
  • 162
  • 270
  • 2
    Also, to keep names in a lower case you can use a custom name transformer: implicit val (modeDecoder, modeEncoder) = { implicit val config: Configuration = Configuration.default.withDefaults.copy(transformConstructorNames = _.toLowerCase); (deriveEnumerationDecoder[Mode], deriveEnumerationEncoder[Mode]) } – Andriy Plokhotnyuk Nov 28 '19 at 10:39

1 Answers1

20

As Andriy Plokhotnyuk notes above, you can use circe-generic-extras:

import io.circe.Codec
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveEnumerationCodec

sealed trait Mode
case object Authentication extends Mode
case object Ocr extends Mode

object Mode {
  private implicit val config: Configuration =
    Configuration.default.copy(transformConstructorNames = _.toLowerCase)

  implicit val modeCodec: Codec[Mode] = deriveEnumerationCodec[Mode]
}

And then:

scala> import io.circe.syntax._
import io.circe.syntax._

scala> (Authentication: Mode).asJson
res1: io.circe.Json = "authentication"

scala> io.circe.Decoder[Mode].decodeJson(res1)
res2: io.circe.Decoder.Result[Mode] = Right(Authentication)

(Note that Codec is new in 0.12β€”for earlier versions you'll have to write out both instances as in Andriy's comment.)

Unless you have a lot of these to maintain, though, I personally think writing the instances out by hand is often better than using circe-generic-extras, and in this case it's not even much more verbose:

import io.circe.{Decoder, Encoder}

sealed trait Mode
case object Authentication extends Mode
case object Ocr extends Mode

object Mode {
  implicit val decodeMode: Decoder[Mode] = Decoder[String].emap {
    case "authentication" => Right(Authentication)
    case "ocr"            => Right(Ocr)
    case other            => Left(s"Invalid mode: $other")
  }

  implicit val encodeMode: Encoder[Mode] = Encoder[String].contramap {
    case Authentication => "authentication"
    case Ocr            => "ocr"
  }
}

Which works exactly the same as the deriveEnumerationCodec version but doesn't require anything but circe-core, is less magical, compiles much faster, etc. Generic derivation can be great for simple case classes with straightforward mappings, but I think people too often try to stretch it to cover all cases when writing instances manually wouldn't be much of a burden and might even be clearer.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • As of circe 0.12.2, the implicit configuration doesn't seem to be automatically picked up, so only the second 'by hand' approach works for me. If you extend Product and Serializable: `sealed trait Mode extends Product with Serializable`, you'll be able to generate the strings with less boilerplate: `implicit val encodeMode: Encoder[Mode] = Encoder[String].contramap { _.productPrefix.toLowerCase }` – Sotomajor Jun 30 '20 at 06:58
  • I like the 2nd approach too, but how do you know whether you forgot a string since there's no exhaustive pattern matching? One other approach is in a companion object of your trait, hold a list of all subclasses and create a def that looks up the associated subtype based on a string id they must all inherit. Then you can use exhaustive pattern matching in a unit test to ensure you always include all cases. This def could be used in the codec, or generally at runtime. The id is the string representation you could serialize with. – ecoe Jul 18 '21 at 02:35