0

I'd like to introduce some types to represent possible values of a field in a larger type. This fields needs to be possible to encode/decode to/from JSON and also be able to be written/read to a database.

I'm still new to Scala and the type I would like is the sum type Status = NotVerified | Correct | Wrong. Since I want to have a string representation associated with each constructor, I created a sealed case class with a String parameter and then objects extending that case class. In order to be able to encode/decode, I also need to have implicits, but I'm not sure how to structure this. I could put them in a new object inside the object, like this:

sealed case class Status(name: String)
object Status {
  object NotVerified extends Status("not_verified")
  object Correct extends Status("correct")
  object Wrong extends Status("wrong")

  object implicits {
    implicit val encodeStatusJson: Encoder[Status] =
      _.name.asJson
    implicit val decodeStatusJson: Decoder[Status] =
      Decoder.decodeString.map(Status(_))

    implicit val encodeStatus: MappedEncoding[Status, String] =
      MappedEncoding[Status, String](_.name)

    implicit val decodeStatus: MappedEncoding[String, Status] =
      MappedEncoding[String, Status](Status(_))
  }
}

… and then explicitly import these where needed, but that's quite … explicit.

What is a good way of organizing such collections of a type + implicits?

beta
  • 2,177
  • 17
  • 29
  • The way you have implemented `Status` is problematic because you can create `Status("not_verified")` but the type will be `Status` rather than `NotVerified`, and a `match` on type will not give the right results. You can make `Status` `abstract` but I suspect that this will break the `Decoder`. – Tim Nov 28 '18 at 14:35
  • @Tim Oh. You're right! Thanks! I don't _really_ want subtypes here. I guess I should use a sealed trait and then case classes extending that? But then the decoders/encoders quill/circe wants will be very repetetive… – beta Nov 28 '18 at 14:41
  • Yes, that is the right way to do a simple enumeration. Make `Status` abstract and create a companion object `Status` with `def apply(name: String): Status = ...` which matches the string and returns the appropriate subclass. Then `Status("not_verified")` will return the right underlying type. – Tim Nov 28 '18 at 15:06

2 Answers2

1

The common approach is to define a sealed trait:

sealed trait Status {
  def name: String
}

object Status {
  case object NotVerified extends Status {
    val name = "not_verified"
  }
  case object Correct extends Status {
    val name = "correct"
  }
  case object Wrong extends Status {
    val name = "wrong"
  }
}

Or a sealed abstract class, which may look nicer in the current Scala versions:

sealed abstract class Status(val name: String)

object Status {
  case object NotVerified extends Status("not_verified")
  case object Correct extends Status("correct")
  case object Wrong extends Status("wrong")
}

To avoid the need to import implicits, they can be placed directly in the companion object of the type. See also the question Where does Scala look for implicits? for more details, especially the section Companion Objects of a Type.

And yes, defining implicits for enumerations like that easily gets repetitive. You have to resort to reflection or macros. I recommend using the Enumeratum library, which also has integrations with Circe and Quill. Here is an example for Circe:

import enumeratum.values._

sealed abstract class Status(val value: String) extends StringEnumEntry {
  def name: String = value
}

object Status extends StringEnum[Status] with StringCirceEnum[Status] {
  val values = findValues

  case object NotVerified extends Status("not_verified")
  case object Correct extends Status("correct")
  case object Wrong extends Status("wrong")
}

And you can use it without defining any encoders/decoders explicitly or importing anything from Status:

scala> import io.circe.syntax._

scala> val status: Status = Status.Correct
status: Status = Correct

scala> status.asJson
res1: io.circe.Json = "correct"

scala> Decoder[Status].decodeJson(Json.fromString("correct"))
res2: io.circe.Decoder.Result[Status] = Right(Correct)
Kolmar
  • 13,241
  • 1
  • 19
  • 24
0

If you add an apply method you can create the appropriate Status from a String, which should make the Decoder work properly. And making Status abstract

sealed abstract class Status(name: String)

object Status {
  object NotVerified extends Status("not_verified")
  object Correct extends Status("correct")
  object Wrong extends Status("wrong")

  def apply(name: String): Status = name match {
    case "not_verified" => NotVerified
    case "correct" => Correct
    case _ => Wrong
  }
}

I think your existing implicits will still work, but I don't know those specific libraries...

Tim
  • 20,192
  • 2
  • 13
  • 23