4

I have the following code, that does not compile:

import java.time.Instant

import io.circe.{Decoder, Encoder}
import io.circe.generic.auto._
import io.circe.syntax._

trait SapHealth {}

case class SapHealthRejected(reason: String) extends SapHealth

case class SapHealthAccepted(sapId: String, requestedAt: Long) extends SapHealth


object SapHealth {

  private val build: SapHealth = SapHealthAccepted(SapmockActor.system.name, Instant.now().getEpochSecond)

  val create: String = build.asJson.noSpaces

  implicit val encodeFieldType: Encoder[SapHealthAccepted] =
    Encoder.forProduct2("sap-id", "requested_at")(SapHealthAccepted.unapply(_).get)

  implicit val decodeFieldType: Decoder[SapHealthAccepted] =
    Decoder.forProduct2("sap-id", "requested_at")(SapHealthAccepted.apply)

}

The compiler complains:

could not find implicit value for parameter encoder: io.circe.Encoder[com.sweetsoft.SapHealth]
[error]   val create: String = build.asJson.noSpaces

What am I missing?

softshipper
  • 32,463
  • 51
  • 192
  • 400

3 Answers3

5

You've specifically up-cast build to SapHealth, but you don't provide an Encoder instance for SapHealth (only SapHealthAccepted), and circe-generic can't derive one because you haven't sealed the trait hierarchy.

The most straightforward solution would be to add sealed:

import io.circe.{Decoder, Encoder}
import io.circe.generic.auto._
import io.circe.syntax._

sealed trait SapHealth {}
case class SapHealthRejected(reason: String) extends SapHealth
case class SapHealthAccepted(sapId: String, requestedAt: Long) extends SapHealth

object SapHealth {
  implicit val encodeFieldType: Encoder[SapHealthAccepted] =
    Encoder.forProduct2("sap-id", "requested_at")(SapHealthAccepted.unapply(_).get)

  implicit val decodeFieldType: Decoder[SapHealthAccepted] =
    Decoder.forProduct2("sap-id", "requested_at")(SapHealthAccepted.apply)

  private val build: SapHealth = SapHealthAccepted("foo", 123L)

  val create: String = build.asJson.noSpaces
}

Note that you also need to rearrange the definitions to avoid running into null-pointer exceptions because of initialization order (if you put create before encodeFieldType, the derived SapHealth encoder will try to use encodeFieldType before it's initialized). With the rearrangement above, this works just fine:

scala> SapHealth.create
res2: String = {"SapHealthAccepted":{"sap-id":"foo","requested_at":123}}

Note that the derived SapHealth encoder is using your custom SapHealthAccepted encoder, which I assume is what you want.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Why does the `sealed` mean? – softshipper May 30 '19 at 14:30
  • 3
    The `sealed` makes it impossible to extend `SapHealth` outside of the current compilation unit. The generic derivation in circe-generic relies on Shapeless's generic representation of ADTs, and Shapeless doesn't provide generic representations for unsealed trait hierarchies. – Travis Brown May 30 '19 at 14:32
2

Trait SapHealth should be sealed.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 3
    Sorry, but I don't find this answer helpful—it doesn't address why sealedness would matter in this case, why `encodeFieldType` doesn't kick in, the initialization order problems, etc. Maybe the OP would be able to get to a solution from here, or maybe not, but it's definitely unlikely to help anyone else. – Travis Brown May 30 '19 at 14:28
  • @TravisBrown You just were faster than me. And after your answer I didn't see any reason to update mine. Good luck. – Dmytro Mitin May 30 '19 at 15:08
  • Don't really get the "here's a placeholder answer while I write a proper one" thing but ¯\_(ツ)_/¯. :) – Travis Brown May 30 '19 at 16:38
1

Just for case class you can use deriveEncoder/deriveDecoder. But in case of complex class you should (as I think) specify it for argument's classes.

https://circe.github.io/circe/codecs/semiauto-derivation.html

import io.circe._, io.circe.generic.semiauto._

case class Foo(a: Int, b: String, c: Boolean)

implicit val fooDecoder: Decoder[Foo] = deriveDecoder[Foo]
implicit val fooEncoder: Encoder[Foo] = deriveEncoder[Foo]
Mikhail Ionkin
  • 568
  • 4
  • 20