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?
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)