Search code examples
jsonscalaakkaakka-streamspray-json

Custom spray-json RootJsonFormat or JsonFormat depending on value in JSON object


In an Akka streams connection I receive JSON objects that looks like this:

{"op":"connection"...}
{"op":"status"...}
..etc

And I have a the following classes setup:

case class ResponseMessage(
  op: Option[OpType],
)

case class ConnectionMessage(
  a: Option[Int],
  b: Option[Int],
) extends ResponseMessage

case class StatusMessage(
  c: Option[Int],
  d: Option[Int],
) extends ResponseMessage

type OpTypes = OpTypes.Value
object OpTypes extends Enumeration {
  val Connection = Value("connection")
  val Status = Value("status")

How can I write custom JsonFormat instances so that depending on the value of op I create the correct type? So that it can be used as such:

> jsValue.convertTo[ResponseMessage] And the outvalue will be either
> ConnectionMessage or StatusMessage?

Solution

  • Note: it's not best practice to extend case class i.e. ResponseMessage. Google "case class inheritance"...

    About your question on JsonFormat, I'd define specific JsonFormats for each subclass, and then define the one for ResponseMessage base on those like:

    import spray.json._
    
    abstract class ResponseMessage(val op: Option[OpTypes])
    object ResponseMessage {
      implicit object jsonFormat extends JsonFormat[ResponseMessage] {
        override def read(json: JsValue): ResponseMessage = json match {
          case JsObject(fields) =>
            fields.get("op") match {
              case Some(JsString("connection")) => json.convertTo[ConnectionMessage]
              case Some(JsString("status")) => json.convertTo[StatusMessage]
              case op => // unknown op
            }
          case _ => // invalid json
        }
    
        override def write(obj: ResponseMessage): JsValue = obj match {
          case connection: ConnectionMessage => connection.toJson
          case status: StatusMessage => status.toJson
        }
      }
    }
    
    case class ConnectionMessage(
                                  a: Option[Int],
                                  b: Option[Int]
                                ) extends ResponseMessage(Some(OpTypes.Connection))
    object ConnectionMessage {
      implicit object jsonFormat extends JsonFormat[ConnectionMessage] {
        override def read(json: JsValue): ConnectionMessage =
          // json -> ConnectionMessage
    
        override def write(obj: ConnectionMessage): JsValue =
          // ConnectionMessage -> json
      }
    }
    
    case class StatusMessage(
                              c: Option[Int],
                              d: Option[Int]
                            ) extends ResponseMessage(Some(OpTypes.Status))
    object StatusMessage {
      implicit object jsonFormat extends JsonFormat[StatusMessage] {
        override def read(json: JsValue): StatusMessage =
          // json -> StatusMessage
    
        override def write(obj: StatusMessage): JsValue =
          // StatusMessage -> json
      }
    }
    
    type OpTypes = OpTypes.Value
    
    object OpTypes extends Enumeration {
      val Connection = Value("connection")
      val Status = Value("status")
    }