Karl's Code

and other code related stuff

Marshalling in Spray.io

Marshalling in spray.io

Issue

When generating output from a Data type Spray looks for implicit Marshaller or UnMarshallers in the scope of the route in question.

There are some default marshallers for most of the common value types such as Int Long String and the Collections, as well as automatic marshallers for case classes

However these may or may not be available, because:-

  • Your type has not been added to the Marshaller typeclass
  • Your type is a case class but some of it’s fields have no marshallern
  • You invoke a Future using the ask pattern and there is no execution context in scope.

Example

  • consider this route from the Spray route Example project
  • the route that get’s the Stats is the problem
"DemoSevice.scala" start:35 mark:38,64-69link
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
trait DemoService extends HttpService {

  // we use the enclosing ActorContext's or ActorSystem's dispatcher for our Futures and Scheduler
  implicit def executionContext = actorRefFactory.dispatcher

  val demoRoute = {
    get {
      pathSingleSlash {
        complete(index)
      } ~
      path("ping") {
        complete("PONG!")
      } ~
      path("stream1") {
        // we detach in order to move the blocking code inside the simpleStringStream into a future
        detach() {
          respondWithMediaType(`text/html`) { // normally Strings are rendered to text/plain, we simply override here
            complete(simpleStringStream)
          }
        }
      } ~
      path("stream2") {
        sendStreamingResponse
      } ~
      path("stream-large-file") {
        encodeResponse(Gzip) {
          getFromFile(largeTempFile)
        }
      } ~
      path("stats") {
        complete {
          actorRefFactory.actorFor("/user/IO-HTTP/listener-0")
            .ask(Http.GetStats)(1.second)
            .mapTo[Stats]
        }
      }
    }
  }
  • the code calls the listener actor, the one that sends the Bound message, and passes it the Http.GetStats message.
  • that actor responds with a Stats object.
  • to prevent blocking the code uses the ask pattern to get a Future[Stats] object, ask returns a Future[Any] so it is cast to Future[Stats]
  • complete looks for a way to marshall Stats to a HttpEntity but runs into trouble….

    • lets break it down
  • Execution Context

  • if you kick of a Future, eg with the ask pattern for the Stats, you must include the following
    • This uses the default execution context of the actor system and makes it implicitly available
"execution context" start:38 mark:39link
1
2
  // we use the enclosing ActorContext's or ActorSystem's dispatcher for our Futures and Scheduler
  implicit def executionContext = actorRefFactory.dispatcher
  • failure to do so will rsult in an error like, note we are trying to create a Marshaller[Future[Stats]]
1
2
[error] DemoService.scala:68: could not find implicit value for parameter marshaller: spray.httpx.marshalling.ToResponseMarshaller[scala.concurrent.Future[spray.can.server.Stats]]
[error]               .mapTo[Stats]
  1. Can’t marshall
  2. the marshalling system for Spray needs an implicit marshaller for Stats
  3. Stats doesn not have one in it’s companion object
  4. in fact it doesn’t have a companion object it is a case class
  5. note the error is identical to the previous which is annoying!
1
2
[error] DemoService.scala:68: could not find implicit value for parameter marshaller: spray.httpx.marshalling.ToResponseMarshaller[scala.concurrent.Future[spray.can.server.Stats]]
[error]               .mapTo[Stats]
  • so we need to bring one into scope, something like this will do although we could use Marshaller.of[Stats] function too with a bit more boilerplate.
"implicit Marshaller" start:168link
1
2
3
4
5
6
7
8
9
10
11
12

val statsMarshaller: Marshaller[Stats] =
    Marshaller.delegate[Stats, String](ContentTypes.`text/plain`) { stats =>
      "Uptime                : " + stats.uptime.formatHMS + '\n' +
      "Total requests        : " + stats.totalRequests + '\n' +
      "Open requests         : " + stats.openRequests + '\n' +
      "Max open requests     : " + stats.maxOpenRequests + '\n' +
      "Total connections     : " + stats.totalConnections + '\n' +
      "Open connections      : " + stats.openConnections + '\n' +
      "Max open connections  : " + stats.maxOpenConnections + '\n' +
      "Requests timed out    : " + stats.requestTimeouts + '\n'
    }

JSON

So with the points above the code will work and on hittingthe /Stats URL will render the stats as a String But what about JSON?

Spray supports JSON an even automatically supports case classes made up from standard value fields all you have to do is

1
Just mix in spray.httpx.SprayJsonSupport or import spray.httpx.SprayJsonSupport._.
  • ie mix that into your route or just import the Object’s vals
    • and it will bring an implicit RootJsonFormat[T] into scope which is a Marshaller
    • and job done right?

Stats has a FiniteDuration

  • look at Stats
  • is not made of the default value types it has a FiniteDuration
1
2
3
4
5
6
7
8
case class MyStats(uptime: FiniteDuration,
                   totalRequests: Long,
                   openRequests: Long,
                   maxOpenRequests: Long,
                   totalConnections: Long,
                   openConnections: Long,
                   maxOpenConnections: Long,
                   requestTimeouts: Long)
  • there is no default RootJsonFormat for that

so we need some help

Create an implicit RootJsonFormat[FiniteDuration]
1
import MyJsonMarshaller._
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object MyJsonMarshaller extends DefaultJsonProtocol {

  // implicit JSON marshaller for FiniteDuration
  implicit object finiteDurationJsonFormat extends RootJsonFormat[FiniteDuration] {
    def write(c: FiniteDuration) = JsNumber(c.toNanos) // toNanos just a hack to create a Long could be cleverer

    def read(value: JsValue) = value match {
      case JsNumber(nanos) => new FiniteDuration(nanos.longValue, TimeUnit.NANOSECONDS)
      case _ => deserializationError("FiniteDuration in Nanos expected")
    }
  }
  
  //use the jsonFormat8() factory to convert Stats
  implicit val statsFormat = jsonFormat8(Stats) 
}

the end.

Comments