Search code examples
jsonscalashapelesscirce

Circe Encoding Sealed Traits and Co Product Case class Fails


I have the following case class:

case class SmartMeterData(
  dateInterval: SmartMeterDataInterval = HalfHourInterval(),
  powerUnit: PowerUnit = KWH,
  smartMeterId: String,
  timestamp: String,
  value: Double
) {
  def timeIntervalInDuration: FiniteDuration = dateInterval match {
    case HalfHourInterval(_) => FiniteDuration(30, TimeUnit.MINUTES)
    case FullHourInterval(_) => FiniteDuration(60, TimeUnit.MINUTES)
  }
}
object SmartMeterData {
  sealed trait SmartMeterDataInterval { def interval: String }
  case class HalfHourInterval(interval: String = "HH") extends SmartMeterDataInterval
  case class FullHourInterval(interval: String = "FH") extends SmartMeterDataInterval

  sealed trait PowerUnit
  case object WH  extends PowerUnit
  case object KWH extends PowerUnit
  case object MWH extends PowerUnit
}

I just wrote a very simple unit test to see if Circe works for my scenario:

"SmartMeterData" should "successfully parse from a valid JSON AST" in {
    val js: String = """
      {
        "dateInterval" : "HH",
        "powerUnit" : "KWH",
        "smartMeterId" : "LCID-001-X-54",
        "timestamp" : "2012-10-12 00:30:00.0000000",
        "value" : 23.0
      }
      """
    val expectedSmartMeterData = SmartMeterData(smartMeterId = "LCID-001-X-54", timestamp = "2012-10-12 00:30:00.0000000", value = 23.0)
    decode[SmartMeterData](js) match {
      case Left(err) => fail(s"Error when parsing valid JSON ${err.toString}")
      case Right(actualSmartMeterData) =>
        assert(actualSmartMeterData equals expectedSmartMeterData)
    }
  }

But it fails with the following error:

Error when parsing valid JSON DecodingFailure(CNil, List(DownField(dateInterval)))

Is there a known limitation with circe where it does not yet work for my case above?


Solution

  • There's no reason that Circe wouldn't work, but the way you've designed the data model, there's basically zero chance of any Scala JSON library that can automatically generate an encoder/decoder working without a lot of manual work.

    For example

    sealed trait SmartMeterDataInterval { def interval: String }
    case class HalfHourInterval(interval: String = "HH") extends SmartMeterDataInterval
    case class FullHourInterval(interval: String = "FH") extends SmartMeterDataInterval
    

    the schema for both, in any Scala JSON library which automatically derives instances for case classes, would be along the lines of

    { "interval": "HH" }
    

    for HalfHourInterval and

     { "interval": "FH" }
    

    for FullHourInterval (i.e. because each has a single string field named interval, they're effectively the same class). In fact your model allows you to have FullHourInterval("HH"), which for at least one method of generating a decoder for an ADT hierarchy in circe (the one in the documentation which uses shapeless) would be the result of decoding { "interval": "HH" }, since that essentially takes the first constructor in lexical order which matches (i.e. FullHourInterval). If the intent is to only allow full- or half-hour intervals, then I'd suggest expressing that as:

    case object HalfHourInterval extends SmartMeterDataInterval { def interval: String = "HH" } 
    case object FullHourInterval extends SmartMeterDataInterval { def interval: String = "FH" }
    

    I'm not directly familiar with how circe encodes case objects, but you can pretty easily define an encoder and decoder for SmartMeterDataInterval:

    object SmartMeterDataInterval {
      implicit val encoder: Encoder[SmartMeterDataInterval] =
        Encoder.encodeString.contramap[SmartMeterDataInterval](_.interval)
      implicit val decoder: Decoder[SmartMeterDataInterval] =
        Decoder.decodeString.emap {
          case "HH" => Right(HalfHourInterval)
          case "FH" => Right(FullHourInterval)
          case _ => Left("not a valid SmartMeterDataInterval")
        }
     }
    

    You would then do something similar to define an Encoder/Decoder for PowerUnit