6
votes

I have an actor which receives JsValue from a websocket in play 2.3. I also have a case class that defines a Reads converter. When I try to pattern match against the case class it always matches against JsValue instead of the case class.

case class Ack(messageType: String, messageId: Int){
  implicit val ackReads: Reads[Ack] = (
      (JsPath \ "message_type").read[String] and
      (JsPath \ "message_id").read[Int]
  )(Ack.apply _)
}


class ChannelActor(out: ActorRef) extends Actor{
  def receive = {
    case a: Ack =>
      println(s"Acknowledged! $a")
    case msg: JsValue =>
      println("Got other jsvalue")
    case _ =>
      println("Got something else")
  }
}

How can I pattern match the JsValue I am receiving from the websocket against the Reads validator in the case class?

Edit: I have found a way to work around this by manually pattern matching against the JsValue to figure out what type I then need to validate. The code looks like this now:

case class Ack(messageType: String, messageId: Int)
object Ack{
  implicit val ackReads: Reads[Ack] = (
    (JsPath \ "message_type").read[String](verifying[String](_ == "ack")) and
    (JsPath \ "message_id").read[Int]
  )(Ack.apply _)

  implicit val ackWrites: Writes[Ack] = (
      (JsPath \ "message_type").write[String] and
      (JsPath \ "message_id").write[Int]
  )(unlift(Ack.unapply))
}


class ChannelActor(out: ActorRef) extends Actor{
  def receive = {
    case msg: JsValue =>
      (msg \ "message_type").asOpt[String] match {
        case Some("ack") => 
          msg.validate[Ack] match{
            case ack: JsSuccess[Ack] => println("got valid ack message")
            case e: JsError => out ! Json.obj("error" -> s"invalid format for ack message ${JsError.toFlatJson(e).toString()}")
          }          
        case None => out ! Json.obj("error" -> "you must send a message_type with your json object")
        case t => out ! Json.obj("error" -> s"unknown message type ${t.get}")
      }      
    case _ => out ! Json.obj("error" -> "unknown message format")
  }
}

This achieves what I want but I have the feeling that it is not the "correct" or most elegant solution to validating JSON messages in Play and will be messy as I implement more message types.

2
This isn't really directed at answering your question, but it seems a bit weird to have your JSON Reads defined inside the case class rather than its companion object. Changing that might be a good starting point.Mikesname

2 Answers

0
votes

see http://www.playframework.com/documentation/2.3.x/ScalaWebSockets

it's in the documentation verbatim

import play.api.libs.json._

implicit val inEventFormat = Json.format[InEvent]
implicit val outEventFormat = Json.format[OutEvent]

import play.api.mvc.WebSocket.FrameFormatter

implicit val inEventFrameFormatter = FrameFormatter.jsonFrame[InEvent]
implicit val outEventFrameFormatter = FrameFormatter.jsonFrame[OutEvent]

import play.api.mvc._
import play.api.Play.current

def socket = WebSocket.acceptWithActor[InEvent, OutEvent] { request => out =>
  MyWebSocketActor.props(out)
}

EDIT: ok got you. the problem is that the Json.format write method does not provide information about the type. let me take a deeper look.

for example

case class Test(a: String)
implicit val f = Json.format[Test]
f.writes(Test1("hey")) // >> {"a":"hey"} note nothing says it's a Test1 instances
0
votes

It is not hard to create such a matcher the following way:

case class ReadsMatch[T](reads: Reads[T]) {
  def unapply(js: JsValue) = reads.reads(js).asOpt
}

Then you can use it in your code as follows:

class ChannelActor(out: ActorRef) extends Actor{
  // create matcher instance
  val ackJson = ReadsMatch[Ack] 
  def receive = {
    // unapply ackJson in the pattern matching case
    case ackJson(Ack(messageType, messageId)) =>
      ??? // do something with extracted messageType and messageId
    case _ => out ! Json.obj("error" -> "unknown message format")
  }
}