How to pattern match against a JsValue with Reads
Asked Answered
W

2

6

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.

Wickman answered 9/8, 2014 at 22:18 Comment(1)
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.Edgell
R
0

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
Reshape answered 10/8, 2014 at 1:52 Comment(2)
I don't quite understand. What are the types "InEvent" and "OutEvent" in this case? My websocket might accept any number of differently formatted json messages. Using acceptWithActor with a specific type would seem to restrict my websocket to only receiving one format of message. Can you provide a concrete example of how to match json input of multiple formats/types using a websocket?Wickman
Perhaps this is meant to be used for a hierarchy of case classes all extending the same trait and a pattern matching json formatter which parses the json to specific case classes, but then we're back to square one because this is exactly the same question (pattern matching and receive are basically the same thing).Pa
I
0

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")
  }
}
Induration answered 19/5, 2016 at 16:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.