Search code examples
scalacirce

Scala Circe infer class


I'm consuming messages from a websocket. They come in one of two formats:

{"typ": "subscription", "channel": "BTC-USD"}

or

{"typ": "ticker", "price": 10213.42}

So I have three case classes to use with Circe's decoder:

case class GenericMessage(typ: String)

case class SubscriptionMessage(channel: String)

case class TickerMessage(price: Double) 

How can I have Circe figure out the type of the message, without nesting lots of decode attempts?


val message: String = ??? // From websocket
decode[GenericMessage](message) match {
    case Left(e) => ??? // Handle error
    case Right(decoded) => decoded.typ match {
        case "subscription" => decode[SubscriptionMessage](message) match {
            case Left(e) => ??? // Handle error
            case Right(subscriptionMessage) => ??? // Handle subscription message
         }
        case "ticker" =>  decode[TickerMessage](message) match {
            case Left(e) => ??? // Handle error
            case Right(tickerMessage) => ??? // Handle ticker message
         }
        case other => ??? // Handle unrecognized message
    }
}


Solution

  • Thanks to @LuisMiguelMejiaSuarez.
    I made the following solution using circe-generic-extras:

    sealed trait WebSocketMessage
    final case class Subscriptions(typ: String, channel: String) extends WebSocketMessage
    final case class Ticker(typ: String, price: Double) extends WebSocketMessage
    
    implicit val config: Configuration = Configuration.default
      .withDiscriminator("type") // Tell circe that the 'type' attribute specifies which case class to decode to
      .copy(transformConstructorNames = _.toLowerCase) // Map 'ticker' string to 'Ticker' class name. Could also pass a more complex mapping function here to use arbitrary class names
      .copy(transformMemberNames = {
          case "typ" => "type" // The word 'type' is a reserved Scala, so class members are renamed to'typ' instead
          case other => Configuration.snakeCaseTransformation(other) // Tell circe to map product_ids field to productIds class member, for example.
      })